diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7f7fa285e1..8968c51cd6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10575,12 +10575,19 @@ Sorry for the inconvenience."; "ChannelReactions.UnsavedChangesAlertDiscard" = "Discard"; "ChannelReactions.UnsavedChangesAlertApply" = "Apply"; "ChannelReactions.ToastMaxReactionsReached" = "You can select at most 100 reactions."; -"ChannelReactions.ToastLevelBoostRequired" = "Your channel needs to reach **Level %1$@** to add **%2$@** custom emoji as reactions.**"; + +"ChannelReactions.ToastLevelBoostRequiredTemplate" = "Your channel needs to reach **%1$@** to add **%2$@** as reactions.**"; +"ChannelReactions.ToastLevelBoostRequiredTemplateLevel_1" = "Level 1"; +"ChannelReactions.ToastLevelBoostRequiredTemplateLevel_any" = "Level %d"; +"ChannelReactions.ToastLevelBoostRequiredTemplateEmojiCount_1" = "1 custom emoji"; +"ChannelReactions.ToastLevelBoostRequiredTemplateEmojiCount_any" = "%d custom emoji"; + "ChannelReactions.GeneralInfoLabel" = "You can add emoji from any emoji pack as a reaction."; "ChannelReactions.ReactionsSectionTitle" = "AVAILABLE REACTIONS"; "ChannelReactions.ReactionsInfoLabel" = "You can also [create your own]() emoji packs and use them."; "ChannelReactions.SaveAction" = "Update Reactions"; "ChannelReactions.LevelRequiredLabel" = "Level %1$@ Required"; +"ChannelReactions.InputPlaceholder" = "Add Reactions..."; "ProfileColorSetup.ResetAction" = "Reset Profile Color"; "ProfileColorSetup.IconSectionTitle" = "ADD ICON TO PROFILE"; diff --git a/Tests/CallUITest/Resources/test2.mp4 b/Tests/CallUITest/Resources/test2.mp4 index c0327e91e3..c3361d0617 100644 Binary files a/Tests/CallUITest/Resources/test2.mp4 and b/Tests/CallUITest/Resources/test2.mp4 differ diff --git a/Tests/CallUITest/Resources/test20.mp4 b/Tests/CallUITest/Resources/test20.mp4 new file mode 100644 index 0000000000..c0327e91e3 Binary files /dev/null and b/Tests/CallUITest/Resources/test20.mp4 differ diff --git a/Tests/CallUITest/Resources/test3.mp4 b/Tests/CallUITest/Resources/test3.mp4 new file mode 100644 index 0000000000..fc128e106b Binary files /dev/null and b/Tests/CallUITest/Resources/test3.mp4 differ diff --git a/Tests/CallUITest/Resources/test4.mp4 b/Tests/CallUITest/Resources/test4.mp4 new file mode 100644 index 0000000000..e34e7bf854 Binary files /dev/null and b/Tests/CallUITest/Resources/test4.mp4 differ diff --git a/Tests/CallUITest/Sources/AppDelegate.swift b/Tests/CallUITest/Sources/AppDelegate.swift index f40c8894f5..dd7ab8d337 100644 --- a/Tests/CallUITest/Sources/AppDelegate.swift +++ b/Tests/CallUITest/Sources/AppDelegate.swift @@ -16,6 +16,8 @@ public final class AppDelegate: NSObject, UIApplicationDelegate { window.rootViewController = ViewController() window.makeKeyAndVisible() + application.internalSetStatusBarStyle(.lightContent, animated: false) + return true } } diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index d9103d2dad..8715889c47 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -5,7 +5,7 @@ import Display import CallScreen import ComponentFlow -public final class ViewController: UIViewController { +public final class ViewController: UIViewController { private var callScreenView: PrivateCallScreen? private var callState: PrivateCallScreen.State = PrivateCallScreen.State( lifecycleState: .connecting, @@ -17,6 +17,9 @@ public final class ViewController: UIViewController { remoteVideo: nil ) + private var currentLayout: (size: CGSize, insets: UIEdgeInsets)? + private var viewLayoutTransition: Transition? + override public func viewDidLoad() { super.viewDidLoad() @@ -66,6 +69,8 @@ public final class ViewController: UIViewController { } if let input = self.callState.localVideo as? FileVideoSource { input.sourceId = input.sourceId == 0 ? 1 : 0 + input.fixedRotationAngle = input.sourceId == 0 ? Float.pi * 0.0 : Float.pi * 0.5 + input.sizeMultiplicator = input.sourceId == 0 ? CGPoint(x: 1.0, y: 1.0) : CGPoint(x: 1.5, y: 1.0) } } callScreenView.videoAction = { [weak self] in @@ -73,7 +78,7 @@ public final class ViewController: UIViewController { return } if self.callState.localVideo == nil { - self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!) + self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test3", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0) } else { self.callState.localVideo = nil } @@ -81,7 +86,7 @@ public final class ViewController: UIViewController { } callScreenView.microhoneMuteAction = { if self.callState.remoteVideo == nil { - self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!) + self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test4", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0) } else { self.callState.remoteVideo = nil } @@ -96,32 +101,44 @@ public final class ViewController: UIViewController { self.callState.localVideo = nil self.update(transition: .spring(duration: 0.4)) } - - self.update(transition: .immediate) } private func update(transition: Transition) { - self.update(size: self.view.bounds.size, transition: transition) + if let (size, insets) = self.currentLayout { + self.update(size: size, insets: insets, transition: transition) + } } - private func update(size: CGSize, transition: Transition) { + private func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { guard let callScreenView = self.callScreenView else { return } transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size)) - let insets: UIEdgeInsets - if size.width < size.height { - insets = UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) - } else { - insets = UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) - } callScreenView.update(size: size, insets: insets, screenCornerRadius: 55.0, state: self.callState, transition: transition) } + override public func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + let safeAreaLayoutGuide = self.view.safeAreaLayoutGuide + + let size = self.view.bounds.size + let insets = UIEdgeInsets(top: safeAreaLayoutGuide.layoutFrame.minY, left: safeAreaLayoutGuide.layoutFrame.minX, bottom: size.height - safeAreaLayoutGuide.layoutFrame.maxY, right: safeAreaLayoutGuide.layoutFrame.minX) + + let transition = self.viewLayoutTransition ?? .immediate + self.viewLayoutTransition = nil + + if let currentLayout = self.currentLayout, currentLayout == (size, insets) { + } else { + self.currentLayout = (size, insets) + self.update(size: size, insets: insets, transition: transition) + } + } + override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - self.update(size: size, transition: .easeInOut(duration: 0.3)) + self.viewLayoutTransition = .easeInOut(duration: 0.3) } } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift index fb7d690634..f123a516d2 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift @@ -11,8 +11,10 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in for (key, _) in attributes { if key == attribute { - addAttribute = false - attributesToRemove.append(key) + if nsRange == range { + addAttribute = false + attributesToRemove.append(key) + } } } } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 973ec22853..a14038b24f 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -591,7 +591,7 @@ public struct Transition { completion?(true) case let .curve(duration, curve): let previousValue: CATransform3D - if let presentation = layer.presentation() { + if layer.animation(forKey: "transform") != nil, let presentation = layer.presentation() { previousValue = presentation.transform } else { previousValue = layer.transform @@ -703,6 +703,33 @@ public struct Transition { ) } } + + public func setZPosition(layer: CALayer, zPosition: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + if layer.zPosition == zPosition { + completion?(true) + return + } + switch self.animation { + case .none: + layer.zPosition = zPosition + layer.removeAnimation(forKey: "zPosition") + completion?(true) + case let .curve(duration, curve): + let previousZPosition = layer.presentation()?.opacity ?? layer.opacity + layer.zPosition = zPosition + layer.animate( + from: previousZPosition as NSNumber, + to: zPosition as NSNumber, + keyPath: "zPosition", + duration: duration, + delay: delay, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index a20da581bf..b2673b2b9b 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -203,9 +203,9 @@ public extension CALayer { } } - func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil, key: String? = nil) { let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) - self.add(animation, forKey: additive ? nil : keyPath) + self.add(animation, forKey: key ?? (additive ? nil : keyPath)) } func animateGroup(_ animations: [CAAnimation], key: String, completion: ((Bool) -> Void)? = nil) { diff --git a/submodules/Display/Source/SimpleLayer.swift b/submodules/Display/Source/SimpleLayer.swift index 8bc691303b..7033bd1f85 100644 --- a/submodules/Display/Source/SimpleLayer.swift +++ b/submodules/Display/Source/SimpleLayer.swift @@ -87,3 +87,32 @@ open class SimpleGradientLayer: CAGradientLayer { fatalError("init(coder:) has not been implemented") } } + +open class SimpleTransformLayer: CATransformLayer { + public var didEnterHierarchy: (() -> Void)? + public var didExitHierarchy: (() -> Void)? + public private(set) var isInHierarchy: Bool = false + + override open func action(forKey event: String) -> CAAction? { + if event == kCAOnOrderIn { + self.isInHierarchy = true + self.didEnterHierarchy?() + } else if event == kCAOnOrderOut { + self.isInHierarchy = false + self.didExitHierarchy?() + } + return nullAction + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/MetalEngine/Sources/MetalEngine.swift b/submodules/MetalEngine/Sources/MetalEngine.swift index bf25c2ad1d..e523e78a80 100644 --- a/submodules/MetalEngine/Sources/MetalEngine.swift +++ b/submodules/MetalEngine/Sources/MetalEngine.swift @@ -115,6 +115,10 @@ open class MetalEngineSubjectLayer: SimpleLayer { fileprivate var internalId: Int = -1 fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation? + #if DEBUG + fileprivate var surfaceChangeFrameCount: Int = 0 + #endif + public override init() { super.init() @@ -780,7 +784,10 @@ public final class MetalEngine { if previousSurfaceId != nil { #if DEBUG - print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))") + layer.surfaceChangeFrameCount += 1 + if layer.surfaceChangeFrameCount > 100 { + print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))") + } #endif } } else { @@ -792,6 +799,10 @@ public final class MetalEngine { #endif } } + } else { + #if DEBUG + layer.surfaceChangeFrameCount = max(0, layer.surfaceChangeFrameCount - 1) + #endif } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 8eddb2ae02..93840afbf3 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -57,13 +57,31 @@ final class ButtonGroupView: OverlayMaskContainerView { fatalError("init(coder:) has not been implemented") } - func update(size: CGSize, buttons: [Button], transition: Transition) { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result === self { + return nil + } + return result + } + + func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], transition: Transition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 let buttonSpacing: CGFloat = 36.0 - let buttonY: CGFloat = size.height - 86.0 - buttonSize + let buttonY: CGFloat + let resultHeight: CGFloat + if controlsHidden { + buttonY = size.height + 12.0 + resultHeight = insets.bottom + 4.0 + } else { + buttonY = size.height - insets.bottom - 52.0 - buttonSize + resultHeight = size.height - buttonY + } var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5) for button in buttons { @@ -137,5 +155,7 @@ final class ButtonGroupView: OverlayMaskContainerView { for key in removeKeys { self.buttonViews.removeValue(forKey: key) } + + return resultHeight } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index 9347cbf35a..12d62e5104 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -64,7 +64,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV if highlighted { self.layer.removeAnimation(forKey: "opacity") - self.layer.removeAnimation(forKey: "sublayerTransform") + self.layer.removeAnimation(forKey: "transform") 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/EmojiTooltipView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiTooltipView.swift new file mode 100644 index 0000000000..6283e72f12 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiTooltipView.swift @@ -0,0 +1,17 @@ +import Foundation +import UIKit +import Display + +final class EmojiTooltipView: UIView { + let size: CGSize + + init(text: String) { + self.size = CGSize() + + super.init(frame: CGRect()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MinimizedVideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MinimizedVideoContainerView.swift deleted file mode 100644 index 90b9434f43..0000000000 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MinimizedVideoContainerView.swift +++ /dev/null @@ -1,144 +0,0 @@ -import Foundation -import UIKit -import Display -import MetalEngine -import ComponentFlow - -final class MinimizedVideoContainerView: UIView { - private struct Params: Equatable { - var size: CGSize - var insets: UIEdgeInsets - - init(size: CGSize, insets: UIEdgeInsets) { - self.size = size - self.insets = insets - } - } - - private struct VideoMetrics: Equatable { - var resolution: CGSize - var rotationAngle: Float - var sourceId: Int - - init(resolution: CGSize, rotationAngle: Float, sourceId: Int) { - self.resolution = resolution - self.rotationAngle = rotationAngle - self.sourceId = sourceId - } - } - - private let videoLayer: PrivateCallVideoLayer - - private var params: Params? - private var videoMetrics: VideoMetrics? - private var appliedVideoMetrics: VideoMetrics? - - var video: VideoSource? { - didSet { - self.video?.updated = { [weak self] in - guard let self else { - return - } - var videoMetrics: VideoMetrics? - if let currentOutput = self.video?.currentOutput { - self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) - } else { - self.videoLayer.video = nil - } - self.videoLayer.setNeedsUpdate() - - if self.videoMetrics != videoMetrics { - self.videoMetrics = videoMetrics - self.update(transition: .easeInOut(duration: 0.2)) - } - } - var videoMetrics: VideoMetrics? - if let currentOutput = self.video?.currentOutput { - self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) - } else { - self.videoLayer.video = nil - } - self.videoLayer.setNeedsUpdate() - - if self.videoMetrics != videoMetrics { - self.videoMetrics = videoMetrics - self.update(transition: .easeInOut(duration: 0.2)) - } - } - } - - override init(frame: CGRect) { - self.videoLayer = PrivateCallVideoLayer() - self.videoLayer.masksToBounds = true - - super.init(frame: frame) - - self.layer.addSublayer(self.videoLayer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return nil - } - - private func update(transition: Transition) { - guard let params = self.params else { - return - } - self.update(params: params, transition: transition) - } - - func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { - let params = Params(size: size, insets: insets) - if self.params == params { - return - } - self.params = params - - self.update(params: params, transition: transition) - } - - private func update(params: Params, transition: Transition) { - guard let videoMetrics = self.videoMetrics else { - return - } - - var transition = transition - if self.appliedVideoMetrics == nil { - transition = .immediate - } - self.appliedVideoMetrics = videoMetrics - - var rotatedResolution = videoMetrics.resolution - var videoIsRotated = false - if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { - rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) - videoIsRotated = true - } - - let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0)) - - let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0)) - let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution - - let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize - let rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize) - - transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center) - transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize)) - transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) - transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize)) - - transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) - transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) - - transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0) - - self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height))) - } -} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift new file mode 100644 index 0000000000..664616101a --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift @@ -0,0 +1,63 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +final class RoundedCornersView: UIImageView { + private let color: UIColor + private var currentCornerRadius: CGFloat? + private var cornerImage: UIImage? + + init(color: UIColor) { + self.color = color + + super.init(image: nil) + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .circular + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func applyStaticCornerRadius() { + guard let cornerRadius = self.currentCornerRadius else { + return + } + if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 { + } else { + let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) + self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + } + self.image = self.cornerImage + self.clipsToBounds = false + self.backgroundColor = nil + self.layer.cornerRadius = 0.0 + } + + func update(cornerRadius: CGFloat, transition: Transition) { + if self.currentCornerRadius == cornerRadius { + return + } + let previousCornerRadius = self.currentCornerRadius + self.currentCornerRadius = cornerRadius + if transition.animation.isImmediate { + self.applyStaticCornerRadius() + } else { + self.image = nil + self.clipsToBounds = true + self.backgroundColor = self.color + if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil { + self.layer.cornerRadius = previousCornerRadius + } + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in + guard let self, completed else { + return + } + self.applyStaticCornerRadius() + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift index 022fae6ebb..7be430df83 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -3,144 +3,522 @@ import UIKit import Display import ComponentFlow import MetalEngine +import SwiftSignalKit private let shadowImage: UIImage? = { UIImage(named: "Call/VideoGradient")?.precomposed() }() -final class VideoContainerView: UIView { - private struct Params: Equatable { - var size: CGSize - var insets: UIEdgeInsets - var cornerRadius: CGFloat - var isMinimized: Bool - var isAnimatingOut: Bool +private final class VideoContainerLayer: SimpleLayer { + let contentsLayer: SimpleLayer + + override init() { + self.contentsLayer = SimpleLayer() - init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool) { - self.size = size - self.insets = insets - self.cornerRadius = cornerRadius - self.isMinimized = isMinimized - self.isAnimatingOut = isAnimatingOut - } + super.init() + + self.addSublayer(self.contentsLayer) } - private struct VideoMetrics: Equatable { - var resolution: CGSize - var rotationAngle: Float + override init(layer: Any) { + self.contentsLayer = SimpleLayer() - init(resolution: CGSize, rotationAngle: Float) { - self.resolution = resolution - self.rotationAngle = rotationAngle - } - } - - private let videoLayer: PrivateCallVideoLayer - let blurredContainerLayer: SimpleLayer - - private let topShadowView: UIImageView - private let bottomShadowView: UIImageView - - private var params: Params? - private var videoMetrics: VideoMetrics? - private var appliedVideoMetrics: VideoMetrics? - - var video: VideoSource? { - didSet { - self.video?.updated = { [weak self] in - guard let self else { - return - } - var videoMetrics: VideoMetrics? - if let currentOutput = self.video?.currentOutput { - self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle) - } else { - self.videoLayer.video = nil - } - self.videoLayer.setNeedsUpdate() - - if self.videoMetrics != videoMetrics { - self.videoMetrics = videoMetrics - self.update(transition: .easeInOut(duration: 0.2)) - } - } - var videoMetrics: VideoMetrics? - if let currentOutput = self.video?.currentOutput { - self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle) - } else { - self.videoLayer.video = nil - } - self.videoLayer.setNeedsUpdate() - - if self.videoMetrics != videoMetrics { - self.videoMetrics = videoMetrics - self.update(transition: .easeInOut(duration: 0.2)) - } - } - } - - override init(frame: CGRect) { - self.videoLayer = PrivateCallVideoLayer() - self.blurredContainerLayer = SimpleLayer() - - self.topShadowView = UIImageView() - self.topShadowView.transform = CGAffineTransformMakeScale(1.0, -1.0) - self.bottomShadowView = UIImageView() - - super.init(frame: frame) - - self.backgroundColor = UIColor.black - self.blurredContainerLayer.backgroundColor = UIColor.black.cgColor - - self.layer.addSublayer(self.videoLayer) - self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer) - - self.topShadowView.image = shadowImage - self.bottomShadowView.image = shadowImage - self.addSubview(self.topShadowView) - self.addSubview(self.bottomShadowView) + super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + func update(size: CGSize, transition: Transition) { + transition.setFrame(layer: self.contentsLayer, frame: CGRect(origin: CGPoint(), size: size)) + } +} + +final class VideoContainerView: HighlightTrackingButton { + enum Key { + case background + case foreground + } + + private struct Params: Equatable { + var size: CGSize + var insets: UIEdgeInsets + var cornerRadius: CGFloat + var controlsHidden: Bool + var isMinimized: Bool + var isAnimatedOut: Bool + + init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) { + self.size = size + self.insets = insets + self.cornerRadius = cornerRadius + self.controlsHidden = controlsHidden + self.isMinimized = isMinimized + self.isAnimatedOut = isAnimatedOut + } + } + + private struct VideoMetrics: Equatable { + var resolution: CGSize + var rotationAngle: Float + var sourceId: Int + + init(resolution: CGSize, rotationAngle: Float, sourceId: Int) { + self.resolution = resolution + self.rotationAngle = rotationAngle + self.sourceId = sourceId + } + } + + private final class FlipAnimationInfo { + let isForward: Bool + + init(isForward: Bool) { + self.isForward = isForward + } + } + + private final class DisappearingVideo { + let flipAnimationInfo: FlipAnimationInfo? + let videoLayer: PrivateCallVideoLayer + let videoMetrics: VideoMetrics + var isAlphaAnimationInitiated: Bool = false + + init(flipAnimationInfo: FlipAnimationInfo?, videoLayer: PrivateCallVideoLayer, videoMetrics: VideoMetrics) { + self.flipAnimationInfo = flipAnimationInfo + self.videoLayer = videoLayer + self.videoMetrics = videoMetrics + } + } + + private enum MinimizedPosition: CaseIterable { + case topLeft + case topRight + case bottomLeft + case bottomRight + } + + let key: Key + + private let videoContainerLayer: VideoContainerLayer + + private var videoLayer: PrivateCallVideoLayer + private var disappearingVideoLayer: DisappearingVideo? + + let blurredContainerLayer: SimpleLayer + + private let shadowContainer: SimpleLayer + private let topShadowLayer: SimpleLayer + private let bottomShadowLayer: SimpleLayer + + private var params: Params? + private var videoMetrics: VideoMetrics? + private var appliedVideoMetrics: VideoMetrics? + + private var highlightedState: Bool = false + + private(set) var isFillingBounds: Bool = false + + private var minimizedPosition: MinimizedPosition = .bottomRight + private var initialDragPosition: CGPoint? + private var dragPosition: CGPoint? + private var dragVelocity: CGPoint = CGPoint() + private var dragPositionAnimatorLink: SharedDisplayLinkDriver.Link? + + private var videoOnUpdatedListener: Disposable? + var video: VideoSource? { + didSet { + if self.video !== oldValue { + self.videoOnUpdatedListener?.dispose() + + self.videoOnUpdatedListener = self.video?.addOnUpdated { [weak self] in + guard let self else { + return + } + var videoMetrics: VideoMetrics? + if let currentOutput = self.video?.currentOutput { + if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId { + self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId)) + } + + self.videoLayer.video = currentOutput + videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) + } else { + self.videoLayer.video = nil + } + self.videoLayer.setNeedsUpdate() + + if self.videoMetrics != videoMetrics { + self.videoMetrics = videoMetrics + self.update(transition: .easeInOut(duration: 0.2)) + } + } + + if oldValue != nil { + self.initiateVideoSourceSwitch(flipAnimationInfo: nil) + } + + var videoMetrics: VideoMetrics? + if let currentOutput = self.video?.currentOutput { + self.videoLayer.video = currentOutput + videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) + } else { + self.videoLayer.video = nil + } + self.videoLayer.setNeedsUpdate() + + if self.videoMetrics != videoMetrics || oldValue != nil { + self.videoMetrics = videoMetrics + self.update(transition: .easeInOut(duration: 0.2)) + } + } + } + } + + var pressAction: (() -> Void)? + + init(key: Key) { + self.key = key + + self.videoContainerLayer = VideoContainerLayer() + self.videoContainerLayer.backgroundColor = nil + self.videoContainerLayer.isOpaque = false + self.videoContainerLayer.contentsLayer.backgroundColor = nil + self.videoContainerLayer.contentsLayer.isOpaque = false + if #available(iOS 13.0, *) { + self.videoContainerLayer.contentsLayer.cornerCurve = .circular + } + + self.videoLayer = PrivateCallVideoLayer() + self.videoLayer.masksToBounds = true + self.videoLayer.isDoubleSided = false + if #available(iOS 13.0, *) { + self.videoLayer.cornerCurve = .circular + } + + self.blurredContainerLayer = SimpleLayer() + + self.shadowContainer = SimpleLayer() + self.topShadowLayer = SimpleLayer() + self.topShadowLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + self.bottomShadowLayer = SimpleLayer() + + super.init(frame: CGRect()) + + self.videoContainerLayer.contentsLayer.addSublayer(self.videoLayer) + self.layer.addSublayer(self.videoContainerLayer) + self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer) + + self.topShadowLayer.contents = shadowImage?.cgImage + self.bottomShadowLayer.contents = shadowImage?.cgImage + self.shadowContainer.addSublayer(self.topShadowLayer) + self.shadowContainer.addSublayer(self.bottomShadowLayer) + self.layer.addSublayer(self.shadowContainer) + + self.highligthedChanged = { [weak self] highlighted in + guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty else { + return + } + var highlightedState = false + if highlighted { + if params.isMinimized { + highlightedState = true + } + } else { + highlightedState = false + } + + if self.highlightedState == highlightedState { + return + } + self.highlightedState = highlightedState + + let measurementSide = min(self.videoContainerLayer.bounds.width, self.videoContainerLayer.bounds.height) + let topScale: CGFloat = (measurementSide - 8.0) / measurementSide + let maxScale: CGFloat = (measurementSide + 2.0) / measurementSide + + if highlightedState { + self.videoContainerLayer.removeAnimation(forKey: "sublayerTransform") + let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeScale(topScale, topScale, 1.0)) + } else { + let t = self.videoContainerLayer.presentation()?.sublayerTransform ?? self.videoContainerLayer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + let transition = Transition(animation: .none) + transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DIdentity) + + self.videoContainerLayer.animateSublayerScale(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.videoContainerLayer.animateSublayerScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let params = self.params else { + return nil + } + if params.isMinimized { + let videoContainerPoint = self.layer.convert(point, to: self.videoContainerLayer) + if self.videoContainerLayer.bounds.contains(videoContainerPoint) { + return self + } else { + return nil + } + } else { + return nil + } + } + + @objc private func pressed() { + self.pressAction?() + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began, .changed: + self.dragVelocity = CGPoint() + if let dragPositionAnimatorLink = self.dragPositionAnimatorLink { + self.dragPositionAnimatorLink = nil + dragPositionAnimatorLink.invalidate() + } + let translation = recognizer.translation(in: self) + + let initialDragPosition: CGPoint + if let current = self.initialDragPosition { + initialDragPosition = current + } else { + initialDragPosition = self.videoContainerLayer.position + self.initialDragPosition = initialDragPosition + } + self.dragPosition = initialDragPosition.offsetBy(dx: translation.x, dy: translation.y) + self.update(transition: .immediate) + case .ended, .cancelled: + self.initialDragPosition = nil + self.dragVelocity = recognizer.velocity(in: self) + + if let params = self.params, let dragPosition = self.dragPosition { + let endPosition = CGPoint( + x: dragPosition.x - self.dragVelocity.x / (1000.0 * log(0.99)), + y: dragPosition.y - self.dragVelocity.y / (1000.0 * log(0.99)) + ) + + var minCornerDistance: (corner: MinimizedPosition, distance: CGFloat)? + for corner in MinimizedPosition.allCases { + let cornerPosition: CGPoint + switch corner { + case .topLeft: + cornerPosition = CGPoint(x: params.insets.left, y: params.insets.top) + case .topRight: + cornerPosition = CGPoint(x: params.size.width - params.insets.right, y: params.insets.top) + case .bottomLeft: + cornerPosition = CGPoint(x: params.insets.left, y: params.size.height - params.insets.bottom) + case .bottomRight: + cornerPosition = CGPoint(x: params.size.width - params.insets.right, y: params.size.height - params.insets.bottom) + } + + let distance = CGPoint(x: endPosition.x - cornerPosition.x, y: endPosition.y - cornerPosition.y) + let scalarDistance = sqrt(distance.x * distance.x + distance.y * distance.y) + if let (_, minDistance) = minCornerDistance { + if scalarDistance < minDistance { + minCornerDistance = (corner, scalarDistance) + } + } else { + minCornerDistance = (corner, scalarDistance) + } + } + if let minCornerDistance { + self.minimizedPosition = minCornerDistance.corner + } + } + + self.dragPositionAnimatorLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.updateDragPositionAnimation(deltaTime: deltaTime) + }) + default: + break + } + } + + private func updateVelocityUsingSpring(currentVelocity: CGPoint, currentPosition: CGPoint, attractor: CGPoint, springConstant: CGFloat, damping: CGFloat, deltaTime: CGFloat) -> CGPoint { + let displacement = CGPoint(x: attractor.x - currentPosition.x, y: attractor.y - currentPosition.y) + let springForce = CGPoint(x: -springConstant * displacement.x, y: -springConstant * displacement.y) + var newVelocity = CGPoint(x: currentVelocity.x + springForce.x * deltaTime, y: currentVelocity.y + springForce.y * deltaTime) + newVelocity = CGPoint(x: newVelocity.x * exp(-damping * deltaTime), y: newVelocity.y * exp(-damping * deltaTime)) + return newVelocity + } + + private func updateDragPositionAnimation(deltaTime: Double) { + guard let params = self.params, let videoMetrics = self.videoMetrics else { + self.dragPosition = nil + self.dragPositionAnimatorLink = nil + return + } + if !params.isMinimized { + self.dragPosition = nil + self.dragPositionAnimatorLink = nil + return + } + guard var dragPosition = self.dragPosition else { + self.dragPosition = nil + self.dragPositionAnimatorLink = nil + return + } + let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: false) + let targetPosition = videoLayout.rotatedVideoFrame.center + + self.dragVelocity = self.updateVelocityUsingSpring( + currentVelocity: self.dragVelocity, + currentPosition: dragPosition, + attractor: targetPosition, + springConstant: -130.0, + damping: 17.0, + deltaTime: CGFloat(deltaTime) + ) + + if sqrt(self.dragVelocity.x * self.dragVelocity.x + self.dragVelocity.y * self.dragVelocity.y) <= 0.1 { + self.dragVelocity = CGPoint() + self.dragPosition = nil + self.dragPositionAnimatorLink = nil + } else { + dragPosition.x += self.dragVelocity.x * CGFloat(deltaTime) + dragPosition.y += self.dragVelocity.y * CGFloat(deltaTime) + + self.dragPosition = dragPosition + } + + self.update(transition: .immediate) + } + + private func initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo?) { + guard let videoMetrics = self.videoMetrics else { + return + } + if let disappearingVideoLayer = self.disappearingVideoLayer { + disappearingVideoLayer.videoLayer.removeFromSuperlayer() + disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer() + } + let previousVideoLayer = self.videoLayer + self.disappearingVideoLayer = DisappearingVideo(flipAnimationInfo: flipAnimationInfo, videoLayer: self.videoLayer, videoMetrics: videoMetrics) + + self.videoLayer = PrivateCallVideoLayer() + self.videoLayer.opacity = previousVideoLayer.opacity + self.videoLayer.masksToBounds = true + self.videoLayer.isDoubleSided = false + if #available(iOS 13.0, *) { + self.videoLayer.cornerCurve = .circular + } + self.videoLayer.cornerRadius = previousVideoLayer.cornerRadius + self.videoLayer.blurredLayer.opacity = previousVideoLayer.blurredLayer.opacity + + self.videoContainerLayer.contentsLayer.addSublayer(self.videoLayer) + self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer) + + self.dragPosition = nil + self.dragPositionAnimatorLink = nil + } + private func update(transition: Transition) { guard let params = self.params else { return } - self.update(params: params, transition: transition) + self.update(previousParams: params, params: params, transition: transition) } - func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool, transition: Transition) { - let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, isMinimized: isMinimized, isAnimatingOut: isAnimatingOut) + func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { + let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) if self.params == params { return } - self.layer.masksToBounds = true - if self.layer.animation(forKey: "cornerRadius") == nil { - self.layer.cornerRadius = self.params?.cornerRadius ?? 0.0 - } - + let previousParams = self.params self.params = params - transition.setCornerRadius(layer: self.layer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in - guard let self, let params = self.params, completed else { - return - } - if !params.isAnimatingOut { - self.layer.masksToBounds = false - self.layer.cornerRadius = 0.0 - } - }) + if let previousParams, previousParams.controlsHidden != params.controlsHidden { + self.dragPosition = nil + self.dragPositionAnimatorLink = nil + } - self.update(params: params, transition: transition) + self.update(previousParams: previousParams, params: params, transition: transition) } - private func update(params: Params, transition: Transition) { + private struct MinimizedLayout { + var videoIsRotated: Bool + var rotatedVideoSize: CGSize + var rotatedVideoResolution: CGSize + var rotatedVideoFrame: CGRect + var videoTransform: CATransform3D + var effectiveVideoFrame: CGRect + } + + private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, applyDragPosition: Bool) -> MinimizedLayout { + var rotatedResolution = videoMetrics.resolution + var videoIsRotated = false + if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { + rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) + videoIsRotated = true + } + + let minimizedBoundingSize: CGFloat = params.controlsHidden ? 140.0 : 240.0 + let videoSize = rotatedResolution.aspectFitted(CGSize(width: minimizedBoundingSize, height: minimizedBoundingSize)) + + let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0)) + let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution + + let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize + + let rotatedVideoFrame: CGRect + if applyDragPosition, let dragPosition = self.dragPosition { + rotatedVideoFrame = videoSize.centered(around: dragPosition) + } else { + switch self.minimizedPosition { + case .topLeft: + rotatedVideoFrame = CGRect(origin: CGPoint(x: params.insets.left, y: params.insets.top), size: videoSize) + case .topRight: + rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.insets.top), size: videoSize) + case .bottomLeft: + rotatedVideoFrame = CGRect(origin: CGPoint(x: params.insets.left, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize) + case .bottomRight: + rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize) + } + } + + let effectiveVideoFrame = videoSize.centered(around: rotatedVideoFrame.center) + + var videoTransform = CATransform3DIdentity + videoTransform.m34 = 1.0 / 600.0 + videoTransform = CATransform3DRotate(videoTransform, CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0) + if params.isAnimatedOut { + videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0) + } + + return MinimizedLayout( + videoIsRotated: videoIsRotated, + rotatedVideoSize: rotatedVideoSize, + rotatedVideoResolution: rotatedVideoResolution, + rotatedVideoFrame: rotatedVideoFrame, + videoTransform: videoTransform, + effectiveVideoFrame: effectiveVideoFrame + ) + } + + private func update(previousParams: Params?, params: Params, transition: Transition) { guard let videoMetrics = self.videoMetrics else { return } @@ -151,43 +529,108 @@ final class VideoContainerView: UIView { self.appliedVideoMetrics = videoMetrics if params.isMinimized { - var rotatedResolution = videoMetrics.resolution - var videoIsRotated = false - if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { - rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) - videoIsRotated = true + self.isFillingBounds = false + + let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: true) + + transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center) + + self.videoContainerLayer.contentsLayer.masksToBounds = true + if self.disappearingVideoLayer != nil { + self.videoContainerLayer.contentsLayer.backgroundColor = UIColor.black.cgColor + } + transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.videoContainerLayer.contentsLayer.masksToBounds = false + self.videoContainerLayer.contentsLayer.backgroundColor = nil + }) + self.videoContainerLayer.update(size: videoLayout.rotatedVideoSize, transition: transition) + + var videoTransition = transition + if self.videoLayer.bounds.isEmpty { + videoTransition = .immediate + } + var animateFlipDisappearingVideo: DisappearingVideo? + if let disappearingVideoLayer = self.disappearingVideoLayer { + self.disappearingVideoLayer = nil + + let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, applyDragPosition: true) + let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize + + if !disappearingVideoLayer.isAlphaAnimationInitiated { + disappearingVideoLayer.isAlphaAnimationInitiated = true + + if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo { + var videoTransform = self.videoContainerLayer.transform + videoTransform = CATransform3DRotate(videoTransform, (flipAnimationInfo.isForward ? 1.0 : -1.0) * CGFloat.pi * 0.9999, 0.0, 1.0, 0.0) + self.videoContainerLayer.transform = videoTransform + + disappearingVideoLayer.videoLayer.zPosition = 1.0 + transition.setZPosition(layer: disappearingVideoLayer.videoLayer, zPosition: -1.0) + + disappearingVideoLayer.videoLayer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + + animateFlipDisappearingVideo = disappearingVideoLayer + disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer() + } else { + let alphaTransition: Transition = .easeInOut(duration: 0.2) + let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer + alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak self, weak disappearingVideoLayerValue] _ in + guard let self, let disappearingVideoLayerValue else { + return + } + disappearingVideoLayerValue.removeFromSuperlayer() + if self.disappearingVideoLayer?.videoLayer === disappearingVideoLayerValue { + self.disappearingVideoLayer = nil + self.update(transition: .immediate) + } + }) + disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer() + + self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.videoLayer.position = disappearingVideoLayer.videoLayer.position + self.videoLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize)) + self.videoLayer.blurredLayer.position = disappearingVideoLayer.videoLayer.blurredLayer.position + self.videoLayer.blurredLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize)) + } + + let disappearingVideoSize = initialDisapparingVideoSize.aspectFilled(videoLayout.rotatedVideoSize) + transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5)) + transition.setBounds(layer: disappearingVideoLayer.videoLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize)) + transition.setPosition(layer: disappearingVideoLayer.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center) + transition.setBounds(layer: disappearingVideoLayer.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize)) } - let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0)) + let animateFlipDisappearingVideoLayer = animateFlipDisappearingVideo?.videoLayer + transition.setTransform(layer: self.videoContainerLayer, transform: videoLayout.videoTransform, completion: { [weak animateFlipDisappearingVideoLayer] _ in + animateFlipDisappearingVideoLayer?.removeFromSuperlayer() + }) - let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0)) - let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution + transition.setPosition(layer: self.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5)) + transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize)) - let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize - let rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize) - let effectiveVideoFrame = videoSize.centered(around: rotatedVideoFrame.center) + transition.setPosition(layer: self.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center) + transition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 0.0) + transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize)) + videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: videoLayout.videoTransform) - transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center) - transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize)) - transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) - transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize)) + if let previousParams, !previousParams.isMinimized { + self.videoContainerLayer.contentsLayer.cornerRadius = previousParams.cornerRadius + } + transition.setCornerRadius(layer: self.videoContainerLayer.contentsLayer, cornerRadius: 18.0, completion: { [weak self] completed in + guard let self, completed, let params = self.params else { + return + } + if params.isMinimized { + self.videoLayer.cornerRadius = 18.0 + } + }) - transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) - transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) - - transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0) - - self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height))) - - let topShadowHeight: CGFloat = floor(effectiveVideoFrame.height * 0.2) - let topShadowFrame = CGRect(origin: effectiveVideoFrame.origin, size: CGSize(width: effectiveVideoFrame.width, height: topShadowHeight)) - transition.setPosition(view: self.topShadowView, position: topShadowFrame.center) - transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(x: effectiveVideoFrame.minX, y: effectiveVideoFrame.maxY - topShadowHeight), size: topShadowFrame.size)) - transition.setAlpha(view: self.topShadowView, alpha: 0.0) - - let bottomShadowHeight: CGFloat = 200.0 - transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight))) - transition.setAlpha(view: self.bottomShadowView, alpha: 0.0) + self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoLayout.rotatedVideoResolution.width), height: Int(videoLayout.rotatedVideoResolution.height))) } else { var rotatedResolution = videoMetrics.resolution var videoIsRotated = false @@ -196,41 +639,109 @@ final class VideoContainerView: UIView { videoIsRotated = true } - var videoSize = rotatedResolution.aspectFitted(params.size) - let boundingAspectRatio = params.size.width / params.size.height - let videoAspectRatio = videoSize.width / videoSize.height - if abs(boundingAspectRatio - videoAspectRatio) < 0.15 { + var videoSize: CGSize + if params.isAnimatedOut { + self.isFillingBounds = true videoSize = rotatedResolution.aspectFilled(params.size) + } else { + videoSize = rotatedResolution.aspectFitted(params.size) + let boundingAspectRatio = params.size.width / params.size.height + let videoAspectRatio = videoSize.width / videoSize.height + self.isFillingBounds = abs(boundingAspectRatio - videoAspectRatio) < 0.15 + if self.isFillingBounds { + videoSize = rotatedResolution.aspectFilled(params.size) + } } let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0)) let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution + let rotatedBoundingSize = videoIsRotated ? CGSize(width: params.size.height, height: params.size.width) : params.size let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize - let rotatedBoundingSize = params.size - let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize) + let rotatedVideoBoundingSize = params.size + let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedVideoBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedVideoBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize) + + self.videoContainerLayer.contentsLayer.masksToBounds = true + if let previousParams, self.videoContainerLayer.contentsLayer.animation(forKey: "cornerRadius") == nil { + if previousParams.isMinimized { + self.videoContainerLayer.contentsLayer.cornerRadius = self.videoLayer.cornerRadius + } else { + self.videoContainerLayer.contentsLayer.cornerRadius = previousParams.cornerRadius + } + } + self.videoLayer.cornerRadius = 0.0 + transition.setCornerRadius(layer: self.videoContainerLayer.contentsLayer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in + guard let self, completed, let params = self.params else { + return + } + if !params.isMinimized && !params.isAnimatedOut { + self.videoContainerLayer.contentsLayer.cornerRadius = 0.0 + } + }) - transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center) - transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) - transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) - transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) + transition.setPosition(layer: self.videoContainerLayer, position: CGPoint(x: params.size.width * 0.5, y: params.size.height * 0.5)) + transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBoundingSize)) + self.videoContainerLayer.update(size: rotatedBoundingSize, transition: transition) - transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) - transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) - - if !params.isAnimatingOut { - self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height))) + var videoTransition = transition + if self.videoLayer.bounds.isEmpty { + videoTransition = .immediate + if !transition.animation.isImmediate { + self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.videoLayer.blurredLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } - let topShadowHeight: CGFloat = 200.0 - let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight)) - transition.setPosition(view: self.topShadowView, position: topShadowFrame.center) - transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size)) - transition.setAlpha(view: self.topShadowView, alpha: 1.0) + if let disappearingVideoLayer = self.disappearingVideoLayer { + self.disappearingVideoLayer = nil + + if !disappearingVideoLayer.isAlphaAnimationInitiated { + disappearingVideoLayer.isAlphaAnimationInitiated = true + + let alphaTransition: Transition = .easeInOut(duration: 0.2) + let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer + alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerValue] _ in + disappearingVideoLayerValue?.removeFromSuperlayer() + }) + let disappearingVideoLayerBlurredLayerValue = disappearingVideoLayer.videoLayer.blurredLayer + alphaTransition.setAlpha(layer: disappearingVideoLayerBlurredLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerBlurredLayerValue] _ in + disappearingVideoLayerBlurredLayerValue?.removeFromSuperlayer() + }) + } + } - let bottomShadowHeight: CGFloat = 200.0 - transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight))) - transition.setAlpha(view: self.bottomShadowView, alpha: 1.0) + transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) + + videoTransition.setFrame(layer: self.videoLayer, frame: rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5))) + videoTransition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) + videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) + videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0) + videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) + + if !params.isAnimatedOut { + self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height))) + } } + + self.shadowContainer.masksToBounds = true + transition.setCornerRadius(layer: self.shadowContainer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in + guard let self, completed else { + return + } + self.shadowContainer.masksToBounds = false + }) + transition.setFrame(layer: self.shadowContainer, frame: CGRect(origin: CGPoint(), size: params.size)) + + let shadowAlpha: CGFloat = (params.controlsHidden || params.isMinimized || params.isAnimatedOut) ? 0.0 : 1.0 + + let topShadowHeight: CGFloat = 200.0 + let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight)) + transition.setPosition(layer: self.topShadowLayer, position: topShadowFrame.center) + transition.setBounds(layer: self.topShadowLayer, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size)) + transition.setAlpha(layer: self.topShadowLayer, alpha: shadowAlpha) + + let bottomShadowHeight: CGFloat = 200.0 + transition.setFrame(layer: self.bottomShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight))) + transition.setAlpha(layer: self.bottomShadowLayer, alpha: shadowAlpha) } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift new file mode 100644 index 0000000000..11cec5dbdf --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoShadowsView.swift @@ -0,0 +1,17 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +final class VideoShadowsView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, transition: Transition) { + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift index c15a7bc427..fb15640519 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift @@ -2,14 +2,17 @@ import AVFoundation import Metal import CoreVideo import Display +import SwiftSignalKit public final class VideoSourceOutput { + public let resolution: CGSize public let y: MTLTexture public let uv: MTLTexture public let rotationAngle: Float public let sourceId: Int - public init(y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) { + public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) { + self.resolution = resolution self.y = y self.uv = uv self.rotationAngle = rotationAngle @@ -20,8 +23,9 @@ public final class VideoSourceOutput { public protocol VideoSource: AnyObject { typealias Output = VideoSourceOutput - var updated: (() -> Void)? { get set } var currentOutput: Output? { get } + + func addOnUpdated(_ f: @escaping () -> Void) -> Disposable } public final class FileVideoSource: VideoSource { @@ -35,13 +39,17 @@ public final class FileVideoSource: VideoSource { private var targetItem: AVPlayerItem? public private(set) var currentOutput: Output? - public var updated: (() -> Void)? + private var onUpdatedListeners = Bag<() -> Void>() private var displayLink: SharedDisplayLinkDriver.Link? public var sourceId: Int = 0 + public var fixedRotationAngle: Float? + public var sizeMultiplicator: CGPoint = CGPoint(x: 1.0, y: 1.0) - public init?(device: MTLDevice, url: URL) { + public init?(device: MTLDevice, url: URL, fixedRotationAngle: Float? = nil) { + self.fixedRotationAngle = fixedRotationAngle + self.device = device CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) @@ -62,11 +70,26 @@ public final class FileVideoSource: VideoSource { return } if self.updateOutput() { - self.updated?() + for onUpdated in self.onUpdatedListeners.copyItems() { + onUpdated() + } } }) } + public func addOnUpdated(_ f: @escaping () -> Void) -> Disposable { + let index = self.onUpdatedListeners.add(f) + + return ActionDisposable { [weak self] in + DispatchQueue.main.async { + guard let self else { + return + } + self.onUpdatedListeners.remove(index) + } + } + } + private func updateOutput() -> Bool { if self.targetItem !== self.queuePlayer.currentItem { self.targetItem?.remove(self.videoOutput) @@ -117,9 +140,15 @@ public final class FileVideoSource: VideoSource { return false } - rotationAngle = Float.pi * 0.5 + if let fixedRotationAngle = self.fixedRotationAngle { + rotationAngle = fixedRotationAngle + } - self.currentOutput = Output(y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId) + var resolution = CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)) + resolution.width = floor(resolution.width * self.sizeMultiplicator.x) + resolution.height = floor(resolution.height * self.sizeMultiplicator.y) + + self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId) return true } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index adfc27257f..89e7217688 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -128,8 +128,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView { private var emojiView: KeyEmojiView? - private var localVideoContainerView: VideoContainerView? - private var remoteVideoContainerView: VideoContainerView? + private let videoContainerBackgroundView: RoundedCornersView + private let overlayContentsVideoContainerBackgroundView: RoundedCornersView + + private var videoContainerViews: [VideoContainerView] = [] private var activeRemoteVideoSource: VideoSource? private var waitingForFirstRemoteVideoFrameDisposable: Disposable? @@ -137,6 +139,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView { private var activeLocalVideoSource: VideoSource? private var waitingForFirstLocalVideoFrameDisposable: Disposable? + private var areControlsHidden: Bool = false + private var swapLocalAndRemoteVideo: Bool = false + private var processedInitialAudioLevelBump: Bool = false private var audioLevelBump: Float = 0.0 @@ -161,6 +166,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.blobLayer = CallBlobsLayer() self.avatarLayer = AvatarLayer() + self.videoContainerBackgroundView = RoundedCornersView(color: .black) + self.overlayContentsVideoContainerBackgroundView = RoundedCornersView(color: UIColor(white: 0.1, alpha: 1.0)) + self.titleView = TextView() self.statusView = StatusView() @@ -169,9 +177,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.layer.addSublayer(self.backgroundLayer) self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer) + self.overlayContentsView.addSubview(self.overlayContentsVideoContainerBackgroundView) + self.layer.addSublayer(self.blobLayer) self.layer.addSublayer(self.avatarLayer) + self.addSubview(self.videoContainerBackgroundView) + self.overlayContentsView.mask = self.maskContents self.addSubview(self.overlayContentsView) @@ -201,6 +213,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } self.audioLevelUpdateSubscription = nil } + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } public required init?(coder: NSCoder) { @@ -245,6 +259,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil { + self.areControlsHidden = !self.areControlsHidden + self.update(transition: .spring(duration: 0.4)) + } + } + } + public func update(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State, transition: Transition) { let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state) if self.params == params { @@ -259,7 +282,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.activeRemoteVideoSource = remoteVideo } else { let firstVideoFrameSignal = Signal { subscriber in - remoteVideo.updated = { [weak remoteVideo] in + return remoteVideo.addOnUpdated { [weak remoteVideo] in guard let remoteVideo else { subscriber.putCompletion() return @@ -268,12 +291,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView { subscriber.putCompletion() } } - - return EmptyDisposable } var shouldUpdate = false self.waitingForFirstRemoteVideoFrameDisposable = (firstVideoFrameSignal - |> timeout(4.0, queue: .mainQueue(), alternate: .complete()) |> deliverOnMainQueue).startStrict(completed: { [weak self] in guard let self else { return @@ -297,7 +317,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.activeLocalVideoSource = localVideo } else { let firstVideoFrameSignal = Signal { subscriber in - localVideo.updated = { [weak localVideo] in + return localVideo.addOnUpdated { [weak localVideo] in guard let localVideo else { subscriber.putCompletion() return @@ -306,12 +326,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView { subscriber.putCompletion() } } - - return EmptyDisposable } var shouldUpdate = false self.waitingForFirstLocalVideoFrameDisposable = (firstVideoFrameSignal - |> timeout(4.0, queue: .mainQueue(), alternate: .complete()) |> deliverOnMainQueue).startStrict(completed: { [weak self] in guard let self else { return @@ -328,6 +345,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } + if self.activeRemoteVideoSource == nil && self.activeLocalVideoSource == nil { + self.areControlsHidden = false + } + self.params = params self.updateInternal(params: params, transition: transition) } @@ -340,34 +361,42 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } private func updateInternal(params: Params, transition: Transition) { - let backgroundFrame = CGRect(origin: CGPoint(), size: params.size) - - let aspect: CGFloat = params.size.width / params.size.height - let sizeNorm: CGFloat = 64.0 - let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm) - let edgeSize: Int = 2 - - let primaryVideoSource: VideoSource? - let secondaryVideoSource: VideoSource? - if let activeRemoteVideoSource = self.activeRemoteVideoSource, let activeLocalVideoSource = self.activeLocalVideoSource { - primaryVideoSource = activeRemoteVideoSource - secondaryVideoSource = activeLocalVideoSource - } else if let activeRemoteVideoSource = self.activeRemoteVideoSource { - primaryVideoSource = activeRemoteVideoSource - secondaryVideoSource = nil - } else if let activeLocalVideoSource = self.activeLocalVideoSource { - primaryVideoSource = activeLocalVideoSource - secondaryVideoSource = nil - } else { - primaryVideoSource = nil - secondaryVideoSource = nil + let genericAlphaTransition: Transition + switch transition.animation { + case .none: + genericAlphaTransition = .immediate + case let .curve(duration, _): + genericAlphaTransition = .easeInOut(duration: min(0.3, duration)) } - let havePrimaryVideo = self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil + let backgroundFrame = CGRect(origin: CGPoint(), size: params.size) - let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(edgeSize) / renderingSize.width * backgroundFrame.width, dy: -CGFloat(edgeSize) / renderingSize.height * backgroundFrame.height) + var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = [] + if self.swapLocalAndRemoteVideo { + if let activeLocalVideoSource = self.activeLocalVideoSource { + activeVideoSources.append((.background, activeLocalVideoSource)) + } + if let activeRemoteVideoSource = self.activeRemoteVideoSource { + activeVideoSources.append((.foreground, activeRemoteVideoSource)) + } + } else { + if let activeRemoteVideoSource = self.activeRemoteVideoSource { + activeVideoSources.append((.background, activeRemoteVideoSource)) + } + if let activeLocalVideoSource = self.activeLocalVideoSource { + activeVideoSources.append((.foreground, activeLocalVideoSource)) + } + } + let havePrimaryVideo = !activeVideoSources.isEmpty - self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2)) + let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden + + let backgroundAspect: CGFloat = params.size.width / params.size.height + let backgroundSizeNorm: CGFloat = 64.0 + let backgroundRenderingSize = CGSize(width: floor(backgroundSizeNorm * backgroundAspect), height: backgroundSizeNorm) + let backgroundEdgeSize: Int = 2 + let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(backgroundEdgeSize) / backgroundRenderingSize.width * backgroundFrame.width, dy: -CGFloat(backgroundEdgeSize) / backgroundRenderingSize.height * backgroundFrame.height) + self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(backgroundRenderingSize.width) + backgroundEdgeSize * 2, height: Int(backgroundRenderingSize.height) + backgroundEdgeSize * 2)) transition.setFrame(layer: self.backgroundLayer, frame: visualBackgroundFrame) transition.setFrame(layer: self.backgroundLayer.blurredLayer, frame: visualBackgroundFrame) @@ -427,15 +456,17 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.speakerAction?() }), at: 0) } - self.buttonGroupView.update(size: params.size, buttons: buttons, transition: transition) + let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, transition: transition) if case let .active(activeState) = params.state.lifecycleState { let emojiView: KeyEmojiView var emojiTransition = transition + var emojiAlphaTransition = genericAlphaTransition if let current = self.emojiView { emojiView = current } else { emojiTransition = transition.withAnimation(.none) + emojiAlphaTransition = genericAlphaTransition.withAnimation(.none) emojiView = KeyEmojiView(emoji: activeState.emojiKey) self.emojiView = emojiView } @@ -445,11 +476,18 @@ public final class PrivateCallScreen: OverlayMaskContainerView { emojiView.animateIn() } } - emojiTransition.setFrame(view: emojiView, frame: CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiView.size.width, y: params.insets.top + 27.0), size: emojiView.size)) + let emojiY: CGFloat + if currentAreControlsHidden { + emojiY = -8.0 - emojiView.size.height + } else { + emojiY = params.insets.top + 12.0 + } + emojiTransition.setFrame(view: emojiView, frame: CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiView.size.width, y: emojiY), size: emojiView.size)) + emojiAlphaTransition.setAlpha(view: emojiView, alpha: currentAreControlsHidden ? 0.0 : 1.0) } else { if let emojiView = self.emojiView { self.emojiView = nil - transition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in + genericAlphaTransition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in emojiView?.removeFromSuperview() }) } @@ -464,112 +502,162 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let avatarFrame = havePrimaryVideo ? expandedAvatarFrame : collapsedAvatarFrame let avatarCornerRadius = havePrimaryVideo ? params.screenCornerRadius : collapsedAvatarSize * 0.5 - let minimizedVideoInsets = UIEdgeInsets(top: 124.0, left: 12.0, bottom: 178.0, right: 12.0) + var minimizedVideoInsets = UIEdgeInsets() + minimizedVideoInsets.top = params.insets.top + (currentAreControlsHidden ? 0.0 : 60.0) + minimizedVideoInsets.left = params.insets.left + 12.0 + minimizedVideoInsets.right = params.insets.right + 12.0 + minimizedVideoInsets.bottom = contentBottomInset + 12.0 - if let primaryVideoSource { - let remoteVideoContainerView: VideoContainerView - if let current = self.remoteVideoContainerView { - remoteVideoContainerView = current + var validVideoContainerKeys: [VideoContainerView.Key] = [] + for i in 0 ..< activeVideoSources.count { + let (videoContainerKey, videoSource) = activeVideoSources[i] + validVideoContainerKeys.append(videoContainerKey) + + var animateIn = false + let videoContainerView: VideoContainerView + if let current = self.videoContainerViews.first(where: { $0.key == videoContainerKey }) { + videoContainerView = current } else { - remoteVideoContainerView = VideoContainerView(frame: CGRect()) - self.remoteVideoContainerView = remoteVideoContainerView - self.insertSubview(remoteVideoContainerView, belowSubview: self.overlayContentsView) - self.overlayContentsView.layer.addSublayer(remoteVideoContainerView.blurredContainerLayer) + animateIn = true + videoContainerView = VideoContainerView(key: videoContainerKey) + switch videoContainerKey { + case .foreground: + self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer) + + self.insertSubview(videoContainerView, belowSubview: self.overlayContentsView) + self.videoContainerViews.append(videoContainerView) + case .background: + if !self.videoContainerViews.isEmpty { + self.overlayContentsView.layer.insertSublayer(videoContainerView.blurredContainerLayer, below: self.videoContainerViews[0].blurredContainerLayer) + + self.insertSubview(videoContainerView, belowSubview: self.videoContainerViews[0]) + self.videoContainerViews.insert(videoContainerView, at: 0) + } else { + self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer) + + self.insertSubview(videoContainerView, belowSubview: self.overlayContentsView) + self.videoContainerViews.append(videoContainerView) + } + } - remoteVideoContainerView.layer.position = self.avatarLayer.position - remoteVideoContainerView.layer.bounds = self.avatarLayer.bounds - remoteVideoContainerView.alpha = 0.0 - remoteVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position - remoteVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds - remoteVideoContainerView.blurredContainerLayer.opacity = 0.0 - remoteVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: false, isAnimatingOut: false, transition: .immediate) + videoContainerView.pressAction = { [weak self] in + guard let self else { + return + } + self.swapLocalAndRemoteVideo = !self.swapLocalAndRemoteVideo + self.update(transition: .easeInOut(duration: 0.25)) + } } - if remoteVideoContainerView.video !== primaryVideoSource { - remoteVideoContainerView.video = primaryVideoSource + if videoContainerView.video !== videoSource { + videoContainerView.video = videoSource } - transition.setPosition(view: remoteVideoContainerView, position: expandedVideoFrame.center) - transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) - transition.setAlpha(view: remoteVideoContainerView, alpha: 1.0) - transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center) - transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) - transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 1.0) - remoteVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: false, isAnimatingOut: false, transition: transition) - } else { - if let remoteVideoContainerView = self.remoteVideoContainerView { - remoteVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition) - transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: avatarFrame.center) - transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) - transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 0.0) - transition.setPosition(view: remoteVideoContainerView, position: avatarFrame.center) - transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) - if remoteVideoContainerView.alpha != 0.0 { - transition.setAlpha(view: remoteVideoContainerView, alpha: 0.0, completion: { [weak self, weak remoteVideoContainerView] completed in - guard let self, let remoteVideoContainerView, completed else { + let videoContainerTransition = transition + if animateIn { + if i == 0 && self.videoContainerViews.count == 1 { + videoContainerView.layer.position = self.avatarLayer.position + videoContainerView.layer.bounds = self.avatarLayer.bounds + videoContainerView.alpha = 0.0 + videoContainerView.blurredContainerLayer.position = self.avatarLayer.position + videoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds + videoContainerView.blurredContainerLayer.opacity = 0.0 + videoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate) + } else { + videoContainerView.layer.position = expandedVideoFrame.center + videoContainerView.layer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size) + videoContainerView.alpha = 0.0 + videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center + videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size) + videoContainerView.blurredContainerLayer.opacity = 0.0 + videoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate) + } + } + + videoContainerTransition.setPosition(view: videoContainerView, position: expandedVideoFrame.center) + videoContainerTransition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) + videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center) + videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) + videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition) + + let alphaTransition: Transition + switch transition.animation { + case .none: + alphaTransition = .immediate + case let .curve(duration, _): + if animateIn { + if i == 0 { + if self.videoContainerViews.count > 1 && self.videoContainerViews[1].isFillingBounds { + alphaTransition = .immediate + } else { + alphaTransition = transition + } + } else { + alphaTransition = .easeInOut(duration: min(0.1, duration)) + } + } else { + alphaTransition = transition + } + } + + alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0) + alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0) + } + + var removedVideoContainerIndices: [Int] = [] + for i in 0 ..< self.videoContainerViews.count { + let videoContainerView = self.videoContainerViews[i] + if !validVideoContainerKeys.contains(videoContainerView.key) { + removedVideoContainerIndices.append(i) + + if self.videoContainerViews.count == 1 { + let alphaTransition: Transition = genericAlphaTransition + + videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition) + transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center) + transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) + transition.setPosition(view: videoContainerView, position: avatarFrame.center) + transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + if videoContainerView.alpha != 0.0 { + alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in + guard let videoContainerView else { + return + } + videoContainerView.removeFromSuperview() + videoContainerView.blurredContainerLayer.removeFromSuperlayer() + }) + alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) + } + } else if i == 0 { + let alphaTransition = genericAlphaTransition + + alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in + guard let videoContainerView else { return } - remoteVideoContainerView.removeFromSuperview() - remoteVideoContainerView.blurredContainerLayer.removeFromSuperlayer() - if self.remoteVideoContainerView === remoteVideoContainerView { - self.remoteVideoContainerView = nil - } + videoContainerView.removeFromSuperview() + videoContainerView.blurredContainerLayer.removeFromSuperlayer() }) + alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) + } else { + let alphaTransition = genericAlphaTransition + + alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in + guard let videoContainerView else { + return + } + videoContainerView.removeFromSuperview() + videoContainerView.blurredContainerLayer.removeFromSuperlayer() + }) + alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) + + videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition) } } } - - if let secondaryVideoSource { - let localVideoContainerView: VideoContainerView - if let current = self.localVideoContainerView { - localVideoContainerView = current - } else { - localVideoContainerView = VideoContainerView(frame: CGRect()) - self.localVideoContainerView = localVideoContainerView - self.insertSubview(localVideoContainerView, belowSubview: self.overlayContentsView) - self.overlayContentsView.layer.addSublayer(localVideoContainerView.blurredContainerLayer) - - localVideoContainerView.layer.position = self.avatarLayer.position - localVideoContainerView.layer.bounds = self.avatarLayer.bounds - localVideoContainerView.alpha = 0.0 - localVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position - localVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds - localVideoContainerView.blurredContainerLayer.opacity = 0.0 - localVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: true, isAnimatingOut: false, transition: .immediate) - } - - if localVideoContainerView.video !== secondaryVideoSource { - localVideoContainerView.video = secondaryVideoSource - } - - transition.setPosition(view: localVideoContainerView, position: expandedVideoFrame.center) - transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) - transition.setAlpha(view: localVideoContainerView, alpha: 1.0) - transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center) - transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) - transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 1.0) - localVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: true, isAnimatingOut: false, transition: transition) - } else { - if let localVideoContainerView = self.localVideoContainerView { - localVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition) - transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: avatarFrame.center) - transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) - transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 0.0) - transition.setPosition(view: localVideoContainerView, position: avatarFrame.center) - transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) - if localVideoContainerView.alpha != 0.0 { - transition.setAlpha(view: localVideoContainerView, alpha: 0.0, completion: { [weak self, weak localVideoContainerView] completed in - guard let self, let localVideoContainerView, completed else { - return - } - localVideoContainerView.removeFromSuperview() - localVideoContainerView.blurredContainerLayer.removeFromSuperlayer() - if self.localVideoContainerView === localVideoContainerView { - self.localVideoContainerView = nil - } - }) - } - } + for index in removedVideoContainerIndices.reversed() { + self.videoContainerViews.remove(at: index) } if self.avatarLayer.image !== params.state.avatarImage { @@ -577,7 +665,17 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } transition.setPosition(layer: self.avatarLayer, position: avatarFrame.center) transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) - self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded:havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition) + self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded: havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition) + + transition.setPosition(view: self.videoContainerBackgroundView, position: avatarFrame.center) + transition.setBounds(view: self.videoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + transition.setAlpha(view: self.videoContainerBackgroundView, alpha: havePrimaryVideo ? 1.0 : 0.0) + self.videoContainerBackgroundView.update(cornerRadius: havePrimaryVideo ? params.screenCornerRadius : avatarCornerRadius, transition: transition) + + transition.setPosition(view: self.overlayContentsVideoContainerBackgroundView, position: avatarFrame.center) + transition.setBounds(view: self.overlayContentsVideoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + transition.setAlpha(view: self.overlayContentsVideoContainerBackgroundView, alpha: havePrimaryVideo ? 1.0 : 0.0) + self.overlayContentsVideoContainerBackgroundView.update(cornerRadius: havePrimaryVideo ? params.screenCornerRadius : avatarCornerRadius, transition: transition) let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize)) transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.midX, y: blobFrame.midY)) @@ -606,14 +704,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView { constrainedWidth: params.size.width - 16.0 * 2.0, transition: transition ) - let titleFrame = CGRect( - origin: CGPoint( - x: (params.size.width - titleSize.width) * 0.5, - y: !havePrimaryVideo ? collapsedAvatarFrame.maxY + 39.0 : params.insets.top + 17.0 - ), - size: titleSize - ) - transition.setFrame(view: self.titleView, frame: titleFrame) let statusState: StatusView.State switch params.state.lifecycleState { @@ -661,6 +751,25 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } let statusSize = self.statusView.update(state: statusState, transition: .immediate) + + let titleY: CGFloat + if currentAreControlsHidden { + titleY = -8.0 - titleSize.height - statusSize.height + } else if havePrimaryVideo { + titleY = params.insets.top + 2.0 + } else { + titleY = collapsedAvatarFrame.maxY + 39.0 + } + let titleFrame = CGRect( + origin: CGPoint( + x: (params.size.width - titleSize.width) * 0.5, + y: titleY + ), + size: titleSize + ) + transition.setFrame(view: self.titleView, frame: titleFrame) + genericAlphaTransition.setAlpha(view: self.titleView, alpha: currentAreControlsHidden ? 0.0 : 1.0) + let statusFrame = CGRect( origin: CGPoint( x: (params.size.width - statusSize.width) * 0.5, @@ -678,6 +787,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } else { transition.setFrame(view: self.statusView, frame: statusFrame) + genericAlphaTransition.setAlpha(view: self.statusView, alpha: currentAreControlsHidden ? 0.0 : 1.0) } if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2 { @@ -690,7 +800,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.addSubview(weakSignalView) } let weakSignalSize = weakSignalView.update(constrainedSize: CGSize(width: params.size.width - 32.0, height: 100.0)) - let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0)), size: weakSignalSize) + let weakSignalY: CGFloat + if currentAreControlsHidden { + weakSignalY = params.insets.top + 2.0 + } else { + weakSignalY = statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0) + } + let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: weakSignalY), size: weakSignalSize) if weakSignalView.bounds.isEmpty { weakSignalView.frame = weakSignalFrame if !transition.animation.isImmediate { diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 08dfdab46a..a3c415cb82 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -513,7 +513,8 @@ final class PeerAllowedReactionsScreenComponent: Component { animateAsReplacement = true } - let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile, loop: false, title: nil, text: presentationData.strings.ChannelReactions_ToastLevelBoostRequired("\(nextCustomReactionCount)", "\(nextCustomReactionCount)").string, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false }) + let text = presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplate(presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateLevel(Int32(nextCustomReactionCount)), presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateEmojiCount(Int32(nextCustomReactionCount))).string + let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile, loop: false, title: nil, text: text, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false }) self.currentUndoController = undoController self.environment?.controller()?.present(undoController, in: .current) } @@ -724,13 +725,12 @@ final class PeerAllowedReactionsScreenComponent: Component { self.reactionInput = reactionInput } - //TOOD:localize let reactionInputSize = reactionInput.update( transition: animateIn ? .immediate : transition, component: AnyComponent(EmojiListInputComponent( context: component.context, theme: environment.theme, - placeholder: "Add Reactions...", + placeholder: environment.strings.ChannelReactions_InputPlaceholder, reactionItems: enabledReactions, isInputActive: self.displayInput, caretPosition: caretPosition,