diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index c17d759f73..1dcd95c9c1 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -142,6 +142,8 @@ public final class ViewController: UIViewController { self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0)) self.callState.remoteVideo = nil self.callState.localVideo = nil + self.callState.isMicrophoneMuted = false + self.callState.isRemoteBatteryLow = false self.update(transition: .spring(duration: 0.4)) } callScreenView.backAction = { [weak self] in @@ -151,6 +153,12 @@ public final class ViewController: UIViewController { self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted self.update(transition: .spring(duration: 0.4)) } + callScreenView.closeAction = { [weak self] in + guard let self else { + return + } + self.callScreenView?.speakerAction?() + } } private func update(transition: Transition) { diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 5fe09436a1..bc489da5e9 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -136,7 +136,7 @@ public final class CallController: ViewController { override public func loadDisplayNode() { if self.sharedContext.immediateExperimentalUISettings.callUIV2 { - self.displayNode = CallControllerNodeV2(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 = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call) } 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) } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 53d8dbbbb5..4a4db0bd29 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -29,8 +29,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private let callScreen: PrivateCallScreen private var callScreenState: PrivateCallScreen.State? - private var shouldStayHiddenUntilConnection: Bool = false - private var callStartTimestamp: Double? private var callState: PresentationCallState? @@ -67,7 +65,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, - shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall ) { @@ -80,8 +77,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.containerView = UIView() self.callScreen = PrivateCallScreen() - self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection - super.init() self.view.addSubview(self.containerView) @@ -123,6 +118,12 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } self.back?() } + self.callScreen.closeAction = { [weak self] in + guard let self else { + return + } + self.dismissedInteractively?() + } self.callScreenState = PrivateCallScreen.State( lifecycleState: .connecting, @@ -130,7 +131,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP shortName: " ", avatarImage: nil, audioOutput: .internalSpeaker, - isMicrophoneMuted: false, + isLocalAudioMuted: false, + isRemoteAudioMuted: false, localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false @@ -145,8 +147,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } self.isMuted = isMuted - if callScreenState.isMicrophoneMuted != isMuted { - callScreenState.isMicrophoneMuted = isMuted + if callScreenState.isLocalAudioMuted != isMuted { + callScreenState.isLocalAudioMuted = isMuted self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.3, curve: .spring)) } @@ -373,6 +375,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP callScreenState.isRemoteBatteryLow = false } + switch callState.remoteAudioState { + case .muted: + callScreenState.isRemoteAudioMuted = true + case .active: + callScreenState.isRemoteAudioMuted = false + } + if self.callScreenState != callScreenState { self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.35, curve: .spring)) @@ -393,6 +402,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + callScreenState.shortName = peer.compactDisplayTitle if self.currentPeer?.smallProfileImage != peer.smallProfileImage { self.peerAvatarDisposable?.dispose() @@ -460,16 +470,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.containerView.layer.removeAnimation(forKey: "scale") self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - if !self.shouldStayHiddenUntilConnection { - self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) - self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } + self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } func animateOut(completion: @escaping () -> Void) { self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - if !self.shouldStayHiddenUntilConnection || self.containerView.alpha > 0.0 { + if self.containerView.alpha > 0.0 { self.containerView.layer.allowsGroupOpacity = true self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in self?.containerView.layer.allowsGroupOpacity = false @@ -499,7 +507,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP transition.updateFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size)) - if let callScreenState = self.callScreenState { + if var callScreenState = self.callScreenState { + if case .terminated = callScreenState.lifecycleState { + callScreenState.isLocalAudioMuted = false + callScreenState.isRemoteAudioMuted = false + callScreenState.isRemoteBatteryLow = false + callScreenState.localVideo = nil + callScreenState.remoteVideo = nil + } self.callScreen.update( size: layout.size, insets: layout.insets(options: [.statusBar]), diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index 6d202c1f72..ac35b7c0f2 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -67,6 +67,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/AppBundle", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index c1a20242eb..0e86f8f821 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -58,8 +58,10 @@ final class ButtonGroupView: OverlayMaskContainerView { private var buttons: [Button]? private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:] - private var noticeViews: [AnyHashable: NoticeView] = [:] + private var closeButtonView: CloseButtonView? + + var closePressed: (() -> Void)? override init(frame: CGRect) { super.init(frame: frame) @@ -79,7 +81,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -163,6 +165,46 @@ final class ButtonGroupView: OverlayMaskContainerView { } var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5) + if displayClose { + let closeButtonView: CloseButtonView + var closeButtonTransition = transition + var animateIn = false + if let current = self.closeButtonView { + closeButtonView = current + } else { + closeButtonTransition = closeButtonTransition.withAnimation(.none) + animateIn = true + + closeButtonView = CloseButtonView() + self.closeButtonView = closeButtonView + self.addSubview(closeButtonView) + closeButtonView.pressAction = { [weak self] in + guard let self else { + return + } + self.closePressed?() + } + } + let closeButtonSize = CGSize(width: minWidth, height: buttonSize) + closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition) + closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize)) + + if animateIn && !transition.animation.isImmediate { + closeButtonView.animateIn() + } + } else { + if let closeButtonView = self.closeButtonView { + self.closeButtonView = nil + if !transition.animation.isImmediate { + closeButtonView.animateOut(completion: { [weak closeButtonView] in + closeButtonView?.removeFromSuperview() + }) + } else { + closeButtonView.removeFromSuperview() + } + } + } + for button in buttons { let title: String let image: UIImage? @@ -213,9 +255,10 @@ final class ButtonGroupView: OverlayMaskContainerView { Transition.immediate.setScale(view: buttonView, scale: 0.001) buttonView.alpha = 0.0 transition.setScale(view: buttonView, scale: 1.0) - transition.setAlpha(view: buttonView, alpha: 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))) buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition) buttonX += buttonSize + buttonSpacing diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift new file mode 100644 index 0000000000..110814ead6 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import UIKitRuntimeUtils + +final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol { + private struct Params: Equatable { + var text: String + var size: CGSize + + init(text: String, size: CGSize) { + self.text = text + self.size = size + } + } + + private let backdropBackgroundView: RoundedCornersView + private let backgroundView: RoundedCornersView + private let backgroundMaskView: UIView + private let backgroundClippingView: UIView + + private let duration: Double = 5.0 + private var fillTime: Double = 0.0 + + private let backgroundTextView: TextView + private let backgroundTextClippingView: UIView + + private let textView: TextView + + var pressAction: (() -> Void)? + + private var params: Params? + private var updateDisplayLink: SharedDisplayLinkDriver.Link? + + let maskContents: UIView + override static var layerClass: AnyClass { + return MirroringLayer.self + } + + override init(frame: CGRect) { + self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true) + self.backdropBackgroundView.update(cornerRadius: 12.0, transition: .immediate) + + self.backgroundView = RoundedCornersView(color: .white, smoothCorners: true) + self.backgroundView.update(cornerRadius: 12.0, transition: .immediate) + self.backgroundView.isUserInteractionEnabled = false + + self.backgroundMaskView = UIView() + self.backgroundMaskView.backgroundColor = .white + self.backgroundView.mask = self.backgroundMaskView + if let filter = makeLuminanceToAlphaFilter() { + self.backgroundMaskView.layer.filters = [filter] + } + + self.backgroundClippingView = UIView() + self.backgroundClippingView.clipsToBounds = true + self.backgroundClippingView.layer.cornerRadius = 12.0 + + self.backgroundTextClippingView = UIView() + self.backgroundTextClippingView.clipsToBounds = true + + self.backgroundTextView = TextView() + self.textView = TextView() + + self.maskContents = UIView() + + self.maskContents.addSubview(self.backdropBackgroundView) + + super.init(frame: frame) + + (self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer + + self.backgroundTextClippingView.addSubview(self.backgroundTextView) + self.backgroundTextClippingView.isUserInteractionEnabled = false + self.addSubview(self.backgroundTextClippingView) + + self.backgroundClippingView.addSubview(self.backgroundView) + self.backgroundClippingView.isUserInteractionEnabled = false + self.addSubview(self.backgroundClippingView) + + self.backgroundMaskView.addSubview(self.textView) + + self.internalHighligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } else { + let t = self.layer.presentation()?.transform ?? layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + let transition = Transition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in + guard let self, completed else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + (self.layer as? MirroringLayer)?.didEnterHierarchy = { [weak self] in + guard let self else { + return + } + if self.fillTime < self.duration && self.updateDisplayLink == nil { + self.updateDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.fillTime = min(self.duration, self.fillTime + deltaTime) + if let params = self.params { + self.update(params: params, transition: .immediate) + } + + if self.fillTime >= self.duration { + self.updateDisplayLink = nil + } + }) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.pressAction?() + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func update(text: String, size: CGSize, transition: Transition) { + let params = Params(text: text, size: size) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: Transition) { + let fillFraction: CGFloat = CGFloat(self.fillTime / self.duration) + + let sideInset: CGFloat = 12.0 + let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .black, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate) + let _ = self.backgroundTextView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .white, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate) + + transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: params.size)) + + let progressWidth: CGFloat = max(0.0, min(params.size.width, floorToScreenPixels(fillFraction * params.size.width))) + let backgroundClippingFrame = CGRect(origin: CGPoint(x: progressWidth, y: 0.0), size: CGSize(width: params.size.width - progressWidth, height: params.size.height)) + transition.setPosition(view: self.backgroundClippingView, position: backgroundClippingFrame.center) + transition.setBounds(view: self.backgroundClippingView, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX, y: 0.0), size: backgroundClippingFrame.size)) + + let backgroundTextClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: params.size.height)) + transition.setPosition(view: self.backgroundTextClippingView, position: backgroundTextClippingFrame.center) + transition.setBounds(view: self.backgroundTextClippingView, bounds: CGRect(origin: CGPoint(), size: backgroundTextClippingFrame.size)) + + let textFrame = CGRect(origin: CGPoint(x: floor((params.size.width - textSize.width) * 0.5), y: floor((params.size.height - textSize.height) * 0.5)), size: textSize) + transition.setFrame(view: self.textView, frame: textFrame) + transition.setFrame(view: self.backgroundTextView, frame: textFrame) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index 12d62e5104..e02f4c9d00 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -63,8 +63,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width if highlighted { - self.layer.removeAnimation(forKey: "opacity") - self.layer.removeAnimation(forKey: "transform") + self.layer.removeAnimation(forKey: "sublayerTransform") let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } else { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift index 3a0cf981bd..f2b0ffd7ec 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift @@ -5,10 +5,10 @@ import ComponentFlow final class EmojiExpandedInfoView: OverlayMaskContainerView { private struct Params: Equatable { - var constrainedWidth: CGFloat + var width: CGFloat - init(constrainedWidth: CGFloat) { - self.constrainedWidth = constrainedWidth + init(width: CGFloat) { + self.width = width } } @@ -129,8 +129,8 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { return nil } - func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize { - let params = Params(constrainedWidth: constrainedWidth) + func update(width: CGFloat, transition: Transition) -> CGSize { + let params = Params(width: width) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.size } @@ -142,16 +142,12 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { private func update(params: Params, transition: Transition) -> CGSize { let buttonHeight: CGFloat = 56.0 - var constrainedWidth = params.constrainedWidth - constrainedWidth = min(constrainedWidth, 300.0) + let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) + let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) - let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition) - let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition) - - let contentWidth: CGFloat = max(titleSize.width, textSize.width) + 26.0 * 2.0 let contentHeight = 78.0 + titleSize.height + 10.0 + textSize.height + 22.0 + buttonHeight - let size = CGSize(width: contentWidth, height: contentHeight) + let size = CGSize(width: params.width, height: contentHeight) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift index 53ada01185..84beb5570c 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift @@ -198,8 +198,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { encoder.setFragmentTexture(blurredTexture, index: 0) - var brightness: Float = 1.0 - var saturation: Float = 1.2 + var brightness: Float = 0.7 + var saturation: Float = 1.3 var overlay: SIMD4 = SIMD4(1.0, 1.0, 1.0, 0.2) encoder.setFragmentBytes(&brightness, length: 4, index: 0) encoder.setFragmentBytes(&saturation, length: 4, index: 1) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift new file mode 100644 index 0000000000..6b54934313 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift @@ -0,0 +1,73 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +final class RatingView: OverlayMaskContainerView { + private let backgroundView: RoundedCornersView + private let textContainer: UIView + private let textView: TextView + + override init(frame: CGRect) { + self.backgroundView = RoundedCornersView(color: .white) + self.textContainer = UIView() + self.textContainer.clipsToBounds = true + self.textView = TextView() + + super.init(frame: frame) + + self.clipsToBounds = true + + self.maskContents.addSubview(self.backgroundView) + + self.textContainer.addSubview(self.textView) + self.addSubview(self.textContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateIn() { + let delay: Double = 0.2 + + self.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) + + self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.backgroundView.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.backgroundView.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + + self.textContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + self.textContainer.layer.cornerRadius = self.bounds.height * 0.5 + self.textContainer.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.textContainer.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in + guard let self, completed else { + return + } + self.textContainer.layer.cornerRadius = 0.0 + }) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + } + + func update(text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + let sideInset: CGFloat = 12.0 + let verticalInset: CGFloat = 6.0 + + let textSize = self.textView.update(string: text, fontSize: 15.0, fontWeight: 0.0, color: .white, constrainedWidth: constrainedWidth - sideInset * 2.0, transition: .immediate) + let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(cornerRadius: floor(size.height * 0.5), transition: transition) + + transition.setFrame(view: self.textContainer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.textView, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)) + + return size + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift index 664616101a..ecd961fbc1 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift @@ -5,17 +5,16 @@ import ComponentFlow final class RoundedCornersView: UIImageView { private let color: UIColor + private let smoothCorners: Bool + private var currentCornerRadius: CGFloat? private var cornerImage: UIImage? - init(color: UIColor) { + init(color: UIColor, smoothCorners: Bool = false) { self.color = color + self.smoothCorners = smoothCorners super.init(image: nil) - - if #available(iOS 13.0, *) { - self.layer.cornerCurve = .circular - } } required init?(coder: NSCoder) { @@ -26,10 +25,23 @@ final class RoundedCornersView: UIImageView { guard let cornerRadius = self.currentCornerRadius else { return } - if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 { + if self.smoothCorners { + let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0) + if let cornerImage = self.cornerImage, cornerImage.size == size { + } else { + self.cornerImage = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath) + context.setFillColor(self.color.cgColor) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5) + } } else { let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) - self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + if let cornerImage = self.cornerImage, cornerImage.size == size { + } else { + self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + } } self.image = self.cornerImage self.clipsToBounds = false @@ -52,6 +64,14 @@ final class RoundedCornersView: UIImageView { if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil { self.layer.cornerRadius = previousCornerRadius } + if #available(iOS 13.0, *) { + if self.smoothCorners { + self.layer.cornerCurve = .continuous + } else { + self.layer.cornerCurve = .circular + } + + } transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in guard let self, completed else { return diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index b67bebc28c..71a785965c 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -100,7 +100,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { public var shortName: String public var avatarImage: UIImage? public var audioOutput: AudioOutput - public var isMicrophoneMuted: Bool + public var isLocalAudioMuted: Bool + public var isRemoteAudioMuted: Bool public var localVideo: VideoSource? public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool @@ -111,7 +112,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { shortName: String, avatarImage: UIImage?, audioOutput: AudioOutput, - isMicrophoneMuted: Bool, + isLocalAudioMuted: Bool, + isRemoteAudioMuted: Bool, localVideo: VideoSource?, remoteVideo: VideoSource?, isRemoteBatteryLow: Bool @@ -121,7 +123,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.shortName = shortName self.avatarImage = avatarImage self.audioOutput = audioOutput - self.isMicrophoneMuted = isMicrophoneMuted + self.isLocalAudioMuted = isLocalAudioMuted + self.isRemoteAudioMuted = isRemoteAudioMuted self.localVideo = localVideo self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow @@ -143,7 +146,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView { if lhs.audioOutput != rhs.audioOutput { return false } - if lhs.isMicrophoneMuted != rhs.isMicrophoneMuted { + if lhs.isLocalAudioMuted != rhs.isLocalAudioMuted { + return false + } + if lhs.isRemoteAudioMuted != rhs.isRemoteAudioMuted { return false } if lhs.localVideo !== rhs.localVideo { @@ -224,6 +230,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { public var microhoneMuteAction: (() -> Void)? public var endCallAction: (() -> Void)? public var backAction: (() -> Void)? + public var closeAction: (() -> Void)? public override init(frame: CGRect) { self.overlayContentsView = UIView() @@ -264,10 +271,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.avatarTransformLayer.addSublayer(self.avatarLayer) self.layer.addSublayer(self.avatarTransformLayer) - /*let edgeTestLayer = EdgeTestLayer() - edgeTestLayer.frame = CGRect(origin: CGPoint(x: 20.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0)) - self.layer.addSublayer(edgeTestLayer)*/ - self.addSubview(self.videoContainerBackgroundView) self.overlayContentsView.mask = self.maskContents @@ -310,6 +313,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } self.backAction?() } + + self.buttonGroupView.closePressed = { [weak self] in + guard let self else { + return + } + self.closeAction?() + } } public required init?(coder: NSCoder) { @@ -497,6 +507,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let backgroundFrame = CGRect(origin: CGPoint(), size: params.size) + let wideContentWidth: CGFloat + if params.size.width < 500.0 { + wideContentWidth = params.size.width - 44.0 * 2.0 + } else { + wideContentWidth = 400.0 + } + var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = [] if self.swapLocalAndRemoteVideo { if let activeLocalVideoSource = self.activeLocalVideoSource { @@ -554,7 +571,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } self.videoAction?() }), - ButtonGroupView.Button(content: .microphone(isMuted: params.state.isMicrophoneMuted), action: { [weak self] in + ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), action: { [weak self] in guard let self else { return } @@ -584,9 +601,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } var notices: [ButtonGroupView.Notice] = [] - if params.state.isMicrophoneMuted { + 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(0 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(1 as Int), text: "Your camera is turned off")) } @@ -594,7 +614,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView { notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "\(params.state.shortName)'s battery is low")) } - let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, notices: notices, transition: transition) + var displayClose = false + if case .terminated = params.state.lifecycleState { + displayClose = true + } + 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? if self.isEmojiKeyExpanded { @@ -632,7 +656,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - let emojiExpandedInfoSize = emojiExpandedInfoView.update(constrainedWidth: params.size.width - (params.insets.left + 16.0) * 2.0, transition: emojiExpandedInfoTransition) + let emojiExpandedInfoSize = emojiExpandedInfoView.update(width: wideContentWidth, transition: emojiExpandedInfoTransition) let emojiExpandedInfoFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiExpandedInfoSize.width) * 0.5), y: params.insets.top + 73.0), size: emojiExpandedInfoSize) emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height)) emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7e6749f8e7..1210fe7148 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -132,8 +132,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { private var groupCallDisposable: Disposable? private var callController: CallController? + private var call: PresentationCall? public let hasOngoingCall = ValuePromise(false) private let callState = Promise(nil) + private var awaitingCallConnectionDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -741,26 +743,49 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable = (callManager.currentCallSignal |> deliverOnMainQueue).start(next: { [weak self] call in - if let strongSelf = self { - if call !== strongSelf.callController?.call { - strongSelf.callController?.dismiss() - strongSelf.callController = nil - strongSelf.hasOngoingCall.set(false) + guard let self else { + return + } + + if call !== self.call { + self.call = call + + self.callController?.dismiss() + self.callController = nil + self.hasOngoingCall.set(false) + + if let call { + self.callState.set(call.state + |> map(Optional.init)) + self.hasOngoingCall.set(true) + setNotificationCall(call) - if let call = call { - mainWindow.hostView.containerView.endEditing(true) - let callController = CallController(sharedContext: strongSelf, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) - strongSelf.callController = callController - strongSelf.mainWindow?.present(callController, on: .calls) - strongSelf.callState.set(call.state - |> map(Optional.init)) - strongSelf.hasOngoingCall.set(true) - setNotificationCall(call) - } else { - strongSelf.callState.set(.single(nil)) - strongSelf.hasOngoingCall.set(false) - setNotificationCall(nil) + if !call.isOutgoing && call.isIntegratedWithCallKit { + self.awaitingCallConnectionDisposable = (call.state + |> filter { state in + switch state.state { + case .ringing: + return false + default: + return true + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.presentControllerWithCurrentCall() + }) + } else{ + self.presentControllerWithCurrentCall() } + } else { + self.callState.set(.single(nil)) + self.hasOngoingCall.set(false) + self.awaitingCallConnectionDisposable?.dispose() + self.awaitingCallConnectionDisposable = nil + setNotificationCall(nil) } } }) @@ -951,6 +976,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() + self.awaitingCallConnectionDisposable?.dispose() } private var didPerformAccountSettingsImport = false @@ -1010,6 +1036,27 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } + private func presentControllerWithCurrentCall() { + guard let call = self.call else { + return + } + + if let currentCallController = self.callController { + if currentCallController.call === call { + self.navigateToCurrentCall() + return + } else { + self.callController = nil + currentCallController.dismiss() + } + } + + self.mainWindow?.hostView.containerView.endEditing(true) + let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) + self.callController = callController + self.mainWindow?.present(callController, on: .calls) + } + public func updateNotificationTokensRegistration() { let sandbox: Bool #if DEBUG