diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 43cbd314f3..42e2fde306 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -614,7 +614,7 @@ private final class DayComponent: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 61068fd483..22f9f2d944 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -725,7 +725,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button( content: contentComponent, - insets: UIEdgeInsets(), action: { guard let strongSelf = self else { return diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index dcb130a4a8..04453d4e04 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -617,7 +617,7 @@ public extension CombinedComponent { return UIView() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { let context = view.getCombinedComponentContext(Self.self) let storedBody: Body @@ -823,4 +823,8 @@ public extension CombinedComponent { static func Guide() -> _ChildComponentGuide { return _ChildComponentGuide() } + + static func StoredActionSlot(_ argumentsType: Arguments.Type) -> ActionSlot { + return ActionSlot() + } } diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift index db68fab951..5f6db9d8b8 100644 --- a/submodules/ComponentFlow/Source/Base/Component.swift +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -118,7 +118,7 @@ public protocol Component: _TypeErasedComponent, Equatable { func makeView() -> View func makeState() -> State - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize } public extension Component { @@ -131,7 +131,9 @@ public extension Component { } func _update(view: UIView, availableSize: CGSize, environment: Any, transition: Transition) -> CGSize { - return self.update(view: view as! Self.View, availableSize: availableSize, environment: environment as! Environment, transition: transition) + let view = view as! Self.View + + return self.update(view: view, availableSize: availableSize, state: view.context(component: self).state, environment: environment as! Environment, transition: transition) } func _isEqual(to other: _TypeErasedComponent) -> Bool { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index a6e832f3c9..83dcf2a517 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -147,6 +147,12 @@ public struct Transition { return result } + public func withAnimation(_ animation: Animation) -> Transition { + var result = self + result.animation = animation + return result + } + public static var immediate: Transition = Transition(animation: .none) public static func easeInOut(duration: Double) -> Transition { diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 14e3712671..bfe7b7086e 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -1,68 +1,126 @@ import Foundation import UIKit -public final class Button: CombinedComponent, Equatable { +public final class Button: Component { public let content: AnyComponent - public let insets: UIEdgeInsets + public let minSize: CGSize? public let action: () -> Void - public init( + convenience public init( content: AnyComponent, - insets: UIEdgeInsets, + action: @escaping () -> Void + ) { + self.init( + content: content, + minSize: nil, + action: action + ) + } + + private init( + content: AnyComponent, + minSize: CGSize?, action: @escaping () -> Void ) { self.content = content - self.insets = insets + self.minSize = nil self.action = action } - + + public func minSize(_ minSize: CGSize?) -> Button { + return Button( + content: self.content, + minSize: minSize, + action: self.action + ) + } + public static func ==(lhs: Button, rhs: Button) -> Bool { if lhs.content != rhs.content { return false } - if lhs.insets != rhs.insets { + if lhs.minSize != rhs.minSize { return false } return true } - - public final class State: ComponentState { - var isHighlighted = false - - override init() { - super.init() + + public final class View: UIButton { + private let contentView: ComponentHostView + + private var component: Button? + private var currentIsHighlighted: Bool = false { + didSet { + if self.currentIsHighlighted != oldValue { + self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } + } } - } - - public func makeState() -> State { - return State() - } - - public static var body: Body { - let content = Child(environment: Empty.self) - - return { context in - let content = content.update( - component: context.component.content, - availableSize: CGSize(width: context.availableSize.width, height: 44.0), transition: context.transition - ) - - let size = CGSize(width: content.size.width + context.component.insets.left + context.component.insets.right, height: content.size.height + context.component.insets.top + context.component.insets.bottom) - - let component = context.component - - context.add(content - .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) - .opacity(context.state.isHighlighted ? 0.2 : 1.0) - .update(Transition.Update { component, view, transition in - view.frame = component.size.centered(around: component._position ?? CGPoint()) - }) - .gesture(.tap { - component.action() - }) + + override init(frame: CGRect) { + self.contentView = ComponentHostView() + self.contentView.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.addSubview(self.contentView) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action() + } + + override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.currentIsHighlighted = true + + return super.beginTracking(touch, with: event) + } + + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { + self.currentIsHighlighted = false + + super.endTracking(touch, with: event) + } + + override public func cancelTracking(with event: UIEvent?) { + self.currentIsHighlighted = false + + super.cancelTracking(with: event) + } + + func update(component: Button, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: availableSize ) + var size = contentSize + if let minSize = component.minSize { + size.width = max(size.width, minSize.width) + size.height = max(size.height, minSize.height) + } + + self.component = component + + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil) + return size } } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } } diff --git a/submodules/ComponentFlow/Source/Components/Circle.swift b/submodules/ComponentFlow/Source/Components/Circle.swift new file mode 100644 index 0000000000..ed5f8a9f34 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/Circle.swift @@ -0,0 +1,60 @@ +import Foundation +import UIKit + +public final class Circle: Component { + public let color: UIColor + public let size: CGSize + public let width: CGFloat + + public init(color: UIColor, size: CGSize, width: CGFloat) { + self.color = color + self.size = size + self.width = width + } + + public static func ==(lhs: Circle, rhs: Circle) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + if lhs.size != rhs.size { + return false + } + if lhs.width != rhs.width { + return false + } + return true + } + + public final class View: UIImageView { + var component: Circle? + var currentSize: CGSize? + + func update(component: Circle, availableSize: CGSize, transition: Transition) -> CGSize { + let size = CGSize(width: min(availableSize.width, component.size.width), height: min(availableSize.height, component.size.height)) + + if self.currentSize != size || self.component != component { + self.currentSize = size + self.component = component + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + if let context = UIGraphicsGetCurrentContext() { + context.setStrokeColor(component.color.cgColor) + context.setLineWidth(component.width) + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.width / 2.0, dy: component.width / 2.0)) + } + self.image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + } + + return size + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index a9ebfc58ec..8c1318925f 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -44,7 +44,7 @@ public final class Image: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/ComponentFlow/Source/Components/Rectangle.swift b/submodules/ComponentFlow/Source/Components/Rectangle.swift index 7760d363e9..e1254ec1de 100644 --- a/submodules/ComponentFlow/Source/Components/Rectangle.swift +++ b/submodules/ComponentFlow/Source/Components/Rectangle.swift @@ -25,7 +25,7 @@ public final class Rectangle: Component { return true } - public func update(view: UIView, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: UIView, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { var size = availableSize if let width = self.width { size.width = min(size.width, width) diff --git a/submodules/ComponentFlow/Source/Components/Text.swift b/submodules/ComponentFlow/Source/Components/Text.swift index 4c7b405ae6..394d59f19c 100644 --- a/submodules/ComponentFlow/Source/Components/Text.swift +++ b/submodules/ComponentFlow/Source/Components/Text.swift @@ -95,7 +95,7 @@ public final class Text: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize) } } diff --git a/submodules/ComponentFlow/Source/Gestures/PanGesture.swift b/submodules/ComponentFlow/Source/Gestures/PanGesture.swift index 38a5e9b538..34b21688d3 100644 --- a/submodules/ComponentFlow/Source/Gestures/PanGesture.swift +++ b/submodules/ComponentFlow/Source/Gestures/PanGesture.swift @@ -5,7 +5,7 @@ public extension Gesture { enum PanGestureState { case began case updated(offset: CGPoint) - case ended + case ended(velocity: CGPoint) } private final class PanGesture: Gesture { @@ -24,7 +24,7 @@ public extension Gesture { case .began: self.action(.began) case .ended, .cancelled: - self.action(.ended) + self.action(.ended(velocity: self.velocity(in: self.view))) case .changed: let offset = self.translation(in: self.view) self.action(.updated(offset: offset)) diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index 68d8f0b7dd..e481573ddd 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -17,7 +17,7 @@ private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? { return nil } -public final class ComponentHostView: UIView { +public final class ComponentHostView: UIView { private var currentComponent: AnyComponent? private var currentContainerSize: CGSize? private var currentSize: CGSize? @@ -33,19 +33,12 @@ public final class ComponentHostView: UIView { } public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, containerSize: CGSize) -> CGSize { - if let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { - if currentContainerSize == containerSize && currentComponent == component { - return currentSize - } - } - self.currentComponent = component - self.currentContainerSize = containerSize - let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize) + let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: containerSize) self.currentSize = size return size } - private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, containerSize: CGSize) -> CGSize { + private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { precondition(!self.isUpdating) self.isUpdating = true @@ -72,6 +65,20 @@ public final class ComponentHostView: UIView { let _ = maybeEnvironment() EnvironmentBuilder._environment = nil } + + let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated() + if isEnvironmentUpdated { + context.erasedEnvironment._isUpdated = false + } + + if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { + if currentContainerSize == containerSize && currentComponent == component { + self.isUpdating = false + return currentSize + } + } + self.currentComponent = component + self.currentContainerSize = containerSize componentState._updated = { [weak self] transition in guard let strongSelf = self else { @@ -79,7 +86,7 @@ public final class ComponentHostView: UIView { } let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: { preconditionFailure() - } as () -> Environment, updateEnvironment: false, containerSize: containerSize) + } as () -> Environment, updateEnvironment: false, forceUpdate: true, containerSize: containerSize) } let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition) diff --git a/submodules/ComponentFlow/Source/Host/RootHostView.swift b/submodules/ComponentFlow/Source/Host/RootHostView.swift index 5a5d9fec91..e0f73bbae7 100644 --- a/submodules/ComponentFlow/Source/Host/RootHostView.swift +++ b/submodules/ComponentFlow/Source/Host/RootHostView.swift @@ -1,134 +1,3 @@ import Foundation import UIKit -public final class RootHostView: UIViewController { - private let content: AnyComponent<(NavigationLayout, EnvironmentType)> - - private var keyboardWillChangeFrameObserver: NSObjectProtocol? - private var inputHeight: CGFloat = 0.0 - - private let environment: Environment - private var componentView: ComponentHostView<(NavigationLayout, EnvironmentType)> - - private var scheduledTransition: Transition? - - public init( - content: AnyComponent<(NavigationLayout, EnvironmentType)>, - @EnvironmentBuilder environment: () -> Environment - ) { - self.content = content - - self.environment = Environment() - self.componentView = ComponentHostView<(NavigationLayout, EnvironmentType)>() - - EnvironmentBuilder._environment = self.environment - let _ = environment() - EnvironmentBuilder._environment = nil - - super.init(nibName: nil, bundle: nil) - - NotificationCenter.default.addObserver(forName: UIApplication.keyboardWillChangeFrameNotification, object: nil, queue: nil, using: { [weak self] notification in - guard let strongSelf = self else { - return - } - guard let keyboardFrame = notification.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect else { - return - } - - var duration: Double = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.0 - if duration > Double.ulpOfOne { - duration = 0.5 - } - let curve: UInt = (notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 7 - - let transition: Transition - if curve == 7 { - transition = Transition(animation: .curve(duration: duration, curve: .spring)) - } else { - transition = Transition(animation: .curve(duration: duration, curve: .easeInOut)) - } - - strongSelf.updateKeyboardLayout(keyboardFrame: keyboardFrame, transition: transition) - }) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.keyboardWillChangeFrameObserver.flatMap(NotificationCenter.default.removeObserver) - } - - private func updateKeyboardLayout(keyboardFrame: CGRect, transition: Transition) { - self.inputHeight = max(0.0, self.view.bounds.height - keyboardFrame.minY) - if self.componentView.isUpdating || true { - if let _ = self.scheduledTransition { - if case .curve = transition.animation { - self.scheduledTransition = transition - } - } else { - self.scheduledTransition = transition - } - self.view.setNeedsLayout() - } else { - self.updateComponent(size: self.view.bounds.size, transition: transition) - } - } - - private func updateComponent(size: CGSize, transition: Transition) { - self.environment._isUpdated = false - - transition.setFrame(view: self.componentView, frame: CGRect(origin: CGPoint(), size: size)) - let _ = self.componentView.update( - transition: transition, - component: self.content, - environment: { - NavigationLayout( - statusBarHeight: size.width > size.height ? 0.0 : 40.0, - inputHeight: self.inputHeight, - bottomNavigationHeight: 22.0 - ) - self.environment[EnvironmentType.self] - }, - containerSize: size - ) - } - - public func updateEnvironment(@EnvironmentBuilder environment: () -> Environment) { - EnvironmentBuilder._environment = self.environment - let _ = environment() - EnvironmentBuilder._environment = nil - - if self.environment.calculateIsUpdated() { - if !self.view.bounds.size.width.isZero { - self.updateComponent(size: self.view.bounds.size, transition: .immediate) - } - } - } - - override public func viewDidLoad() { - super.viewDidLoad() - - self.view.addSubview(self.componentView) - - if !self.view.bounds.size.width.isZero { - self.updateComponent(size: self.view.bounds.size, transition: .immediate) - } - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - if let scheduledTransition = self.scheduledTransition { - self.scheduledTransition = nil - self.updateComponent(size: self.view.bounds.size, transition: scheduledTransition) - } - } - - override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - self.updateComponent(size: size, transition: coordinator.isAnimated ? .easeInOut(duration: 0.3) : .immediate) - } -} diff --git a/submodules/ComponentFlow/Source/Utils/ActionSlot.swift b/submodules/ComponentFlow/Source/Utils/ActionSlot.swift new file mode 100644 index 0000000000..b2419ed034 --- /dev/null +++ b/submodules/ComponentFlow/Source/Utils/ActionSlot.swift @@ -0,0 +1,28 @@ +import Foundation + +public final class Action { + public let action: (Arguments) -> Void + + public init(_ action: @escaping (Arguments) -> Void) { + self.action = action + } + + public func callAsFunction(_ arguments: Arguments) { + self.action(arguments) + } +} + +public final class ActionSlot { + private var target: ((Arguments) -> Void)? + + init() { + } + + public func connect(_ target: @escaping (Arguments) -> Void) { + self.target = target + } + + public func invoke(_ arguments: Arguments) { + self.target?(arguments) + } +} diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index e5b90f72b0..598395cb3b 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -17,9 +17,9 @@ public final class LottieAnimationComponent: Component { } public let animation: Animation - public let size: CGSize + public let size: CGSize? - public init(animation: Animation, size: CGSize) { + public init(animation: Animation, size: CGSize?) { self.animation = animation self.size = size } @@ -41,8 +41,6 @@ public final class LottieAnimationComponent: Component { private var animationView: LOTAnimationView? func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { - let size = CGSize(width: min(component.size.width, availableSize.width), height: min(component.size.height, availableSize.height)) - if self.currentAnimation != component.animation { if let animationView = self.animationView, animationView.isAnimationPlaying { animationView.completionBlock = { [weak self] _ in @@ -64,8 +62,6 @@ public final class LottieAnimationComponent: Component { view.backgroundColor = .clear view.isOpaque = false - //view.logHierarchyKeypaths() - for (key, value) in component.animation.colors { let colorCallback = LOTColorValueCallback(color: value.cgColor) self.colorCallbacks.append(colorCallback) @@ -78,8 +74,18 @@ public final class LottieAnimationComponent: Component { } } + var animationSize = CGSize() + if let animationView = self.animationView, let sceneModel = animationView.sceneModel { + animationSize = sceneModel.compBounds.size + } + if let customSize = component.size { + animationSize = customSize + } + + let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height)) + if let animationView = self.animationView { - animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - component.size.width) / 2.0), y: floor((size.height - component.size.height) / 2.0)), size: component.size) + animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize) if !animationView.isAnimationPlaying { animationView.play { _ in @@ -95,7 +101,7 @@ public final class LottieAnimationComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift b/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift index b942ac6926..e16c981130 100644 --- a/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift +++ b/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift @@ -107,7 +107,7 @@ public final class ProgressIndicatorComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 1ab09e7555..16c0e3ec0a 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -86,26 +86,6 @@ public final class NavigationBarPresentationData { } } -private func backArrowImage(color: UIColor) -> UIImage? { - var red: CGFloat = 0.0 - var green: CGFloat = 0.0 - var blue: CGFloat = 0.0 - var alpha: CGFloat = 0.0 - color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) - - let key = (Int32(alpha * 255.0) << 24) | (Int32(red * 255.0) << 16) | (Int32(green * 255.0) << 8) | Int32(blue * 255.0) - if let image = backArrowImageCache[key] { - return image - } else { - if let image = NavigationBarTheme.generateBackArrowImage(color: color) { - backArrowImageCache[key] = image - return image - } else { - return nil - } - } -} - enum NavigationPreviousAction: Equatable { case item(UINavigationItem) case close @@ -278,6 +258,26 @@ open class NavigationBar: ASDisplayNode { public static var defaultSecondaryContentHeight: CGFloat { return 38.0 } + + static func backArrowImage(color: UIColor) -> UIImage? { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let key = (Int32(alpha * 255.0) << 24) | (Int32(red * 255.0) << 16) | (Int32(green * 255.0) << 8) | Int32(blue * 255.0) + if let image = backArrowImageCache[key] { + return image + } else { + if let image = NavigationBarTheme.generateBackArrowImage(color: color) { + backArrowImageCache[key] = image + return image + } else { + return nil + } + } + } public static let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) @@ -880,7 +880,7 @@ open class NavigationBar: ASDisplayNode { self.rightButtonNode.color = self.presentationData.theme.buttonColor self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.rightButtonNode.rippleColor = self.presentationData.theme.primaryTextColor.withAlphaComponent(0.05) - self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) + self.backButtonArrow.image = NavigationBar.backArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: NavigationBar.titleFont, textColor: self.presentationData.theme.primaryTextColor) self.titleNode.accessibilityLabel = title @@ -973,7 +973,7 @@ open class NavigationBar: ASDisplayNode { self.rightButtonNode.color = self.presentationData.theme.buttonColor self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.rightButtonNode.rippleColor = self.presentationData.theme.primaryTextColor.withAlphaComponent(0.05) - self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) + self.backButtonArrow.image = NavigationBar.backArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: NavigationBar.titleFont, textColor: self.presentationData.theme.primaryTextColor) self.titleNode.accessibilityLabel = title @@ -1310,7 +1310,7 @@ open class NavigationBar: ASDisplayNode { public func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? { if self.backButtonArrow.supernode != nil { let node = ASImageNode() - node.image = backArrowImage(color: accentColor) + node.image = NavigationBar.backArrowImage(color: accentColor) node.frame = self.backButtonArrow.frame node.displayWithoutProcessing = true node.displaysAsynchronously = false diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 6ad77e802e..34d78b9b3d 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -64,7 +64,7 @@ public final class MultilineText: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -126,7 +126,7 @@ public final class LottieAnimationComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -249,7 +249,7 @@ private final class ScrollingTooltipAnimationComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -400,7 +400,7 @@ public final class TooltipComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -476,7 +476,7 @@ private final class RoundedRectangle: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -550,7 +550,7 @@ private final class ShadowRoundedRectangle: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } @@ -701,7 +701,7 @@ public final class RollingText: Component { return View() } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize) } } diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 41a9bb797e..0061b1aa72 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -329,7 +329,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 { panelData = nil } else { - panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil) ? nil : availableState + panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil && availableState?.info.isStream == false) ? nil : availableState } let wasEmpty = strongSelf.groupCallPanelData == nil diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index b5877ec8e6..bedebbc74d 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -96,6 +96,7 @@ swift_library( "//submodules/ChatTitleActivityNode:ChatTitleActivityNode", "//third-party/libyuv:LibYuvBinding", "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index a8c007a790..bcf45903eb 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -4,39 +4,396 @@ import ComponentFlow import Display import AccountContext import SwiftSignalKit +import AVKit +import TelegramCore +import Postbox +import ShareController +import UndoUI +import TelegramPresentationData +import LottieAnimationComponent + +final class NavigationBackButtonComponent: Component { + let text: String + let color: UIColor + + init(text: String, color: UIColor) { + self.text = text + self.color = color + } + + static func ==(lhs: NavigationBackButtonComponent, rhs: NavigationBackButtonComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + return false + } + + public final class View: UIView { + private let arrowView: UIImageView + private let textView: ComponentHostView + + private var component: NavigationBackButtonComponent? + + override init(frame: CGRect) { + self.arrowView = UIImageView() + self.textView = ComponentHostView() + + super.init(frame: frame) + + self.addSubview(self.arrowView) + self.addSubview(self.textView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: NavigationBackButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + let spacing: CGFloat = 6.0 + let innerArrowInset: CGFloat = -8.0 + + if self.component?.color != component.color { + self.arrowView.image = NavigationBarTheme.generateBackArrowImage(color: component.color) + } + + self.component = component + + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(Text( + text: component.text, + font: Font.regular(17.0), + color: component.color + )), + environment: {}, + containerSize: availableSize + ) + + var leftInset: CGFloat = 0.0 + var size = textSize + if let arrowImage = self.arrowView.image { + size.width += innerArrowInset + arrowImage.size.width + spacing + size.height = max(size.height, arrowImage.size.height) + + self.arrowView.frame = CGRect(origin: CGPoint(x: innerArrowInset, y: floor((size.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + leftInset += innerArrowInset + arrowImage.size.width + spacing + } + self.textView.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class BundleIconComponent: Component { + let name: String + let tintColor: UIColor? + + init(name: String, tintColor: UIColor?) { + self.name = name + self.tintColor = tintColor + } + + static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool { + if lhs.name != rhs.name { + return false + } + if lhs.tintColor != rhs.tintColor { + return false + } + return false + } + + public final class View: UIImageView { + private var component: BundleIconComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BundleIconComponent, availableSize: CGSize, transition: Transition) -> CGSize { + if self.component?.name != component.name || self.component?.tintColor != component.tintColor { + if let tintColor = component.tintColor { + self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil) + } else { + self.image = UIImage(bundleImageName: component.name) + } + } + self.component = component + + let imageSize = self.image?.size ?? CGSize() + + return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height)) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} private final class NavigationBarComponent: CombinedComponent { + let topInset: CGFloat + let sideInset: CGFloat + let leftItem: AnyComponent? + let rightItems: [AnyComponentWithIdentity] + let centerItem: AnyComponent? + + init( + topInset: CGFloat, + sideInset: CGFloat, + leftItem: AnyComponent?, + rightItems: [AnyComponentWithIdentity], + centerItem: AnyComponent? + ) { + self.topInset = topInset + self.sideInset = sideInset + self.leftItem = leftItem + self.rightItems = rightItems + self.centerItem = centerItem + } + + static func ==(lhs: NavigationBarComponent, rhs: NavigationBarComponent) -> Bool { + if lhs.topInset != rhs.topInset { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.leftItem != rhs.leftItem { + return false + } + if lhs.rightItems != rhs.rightItems { + return false + } + if lhs.centerItem != rhs.centerItem { + return false + } + + return true + } + + static var body: Body { + let background = Child(Rectangle.self) + let leftItem = Child(environment: Empty.self) + let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let centerItem = Child(environment: Empty.self) + + return { context in + var availableWidth = context.availableSize.width + let sideInset: CGFloat = 16.0 + context.component.sideInset + + let contentHeight: CGFloat = 44.0 + let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) + + let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition) + + let leftItem = context.component.leftItem.flatMap { leftItemComponent in + return leftItem.update( + component: leftItemComponent, + availableSize: CGSize(width: availableWidth, height: contentHeight), + transition: context.transition + ) + } + if let leftItem = leftItem { + availableWidth -= leftItem.size.width + } + + var rightItemList: [_UpdatedChildComponent] = [] + for item in context.component.rightItems { + let item = rightItems[item.id].update( + component: item.component, + availableSize: CGSize(width: availableWidth, height: contentHeight), + transition: context.transition + ) + rightItemList.append(item) + availableWidth -= item.size.width + } + + let centerItem = context.component.centerItem.flatMap { centerItemComponent in + return centerItem.update( + component: centerItemComponent, + availableSize: CGSize(width: availableWidth, height: contentHeight), + transition: context.transition + ) + } + if let centerItem = centerItem { + availableWidth -= centerItem.size.width + } + + context.add(background + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + + var centerLeftInset = sideInset + if let leftItem = leftItem { + context.add(leftItem + .position(CGPoint(x: sideInset + leftItem.size.width / 2.0, y: context.component.topInset + contentHeight / 2.0)) + ) + centerLeftInset += leftItem.size.width + 4.0 + } + + var centerRightInset = sideInset + var rightItemX = context.availableSize.width - sideInset + for item in rightItemList.reversed() { + context.add(item + .position(CGPoint(x: rightItemX - item.size.width / 2.0, y: context.component.topInset + contentHeight / 2.0)) + ) + rightItemX -= item.size.width + 4.0 + centerRightInset += item.size.width + 4.0 + } + + let maxCenterInset = max(centerLeftInset, centerRightInset) + if let centerItem = centerItem { + context.add(centerItem + .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: context.component.topInset + contentHeight / 2.0)) + ) + } + + return size + } + } +} + +private final class OriginInfoComponent: CombinedComponent { + let title: String + let subtitle: String + + init( + title: String, + subtitle: String + ) { + self.title = title + self.subtitle = subtitle + } + + static func ==(lhs: OriginInfoComponent, rhs: OriginInfoComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + + return true + } + + static var body: Body { + let title = Child(Text.self) + let subtitle = Child(Text.self) + + return { context in + let spacing: CGFloat = 0.0 + + let title = title.update( + component: Text( + text: context.component.title, font: Font.semibold(17.0), color: .white), + availableSize: context.availableSize, + transition: context.transition + ) + + let subtitle = subtitle.update( + component: Text( + text: context.component.subtitle, font: Font.regular(14.0), color: .white), + availableSize: context.availableSize, + transition: context.transition + ) + + var size = CGSize(width: max(title.size.width, subtitle.size.width), height: title.size.height + spacing + subtitle.size.height) + size.width = min(size.width, context.availableSize.width) + size.height = min(size.height, context.availableSize.height) + + context.add(title + .position(CGPoint(x: size.width / 2.0, y: title.size.height / 2.0)) + ) + context.add(subtitle + .position(CGPoint(x: size.width / 2.0, y: title.size.height + spacing + subtitle.size.height / 2.0)) + ) + + return size + } + } +} + +private final class ToolbarComponent: CombinedComponent { + let bottomInset: CGFloat + let sideInset: CGFloat let leftItem: AnyComponent? let rightItem: AnyComponent? let centerItem: AnyComponent? init( + bottomInset: CGFloat, + sideInset: CGFloat, leftItem: AnyComponent?, rightItem: AnyComponent?, centerItem: AnyComponent? ) { + self.bottomInset = bottomInset + self.sideInset = sideInset self.leftItem = leftItem self.rightItem = rightItem self.centerItem = centerItem } - static func ==(lhs: NavigationBarComponent, rhs: NavigationBarComponent) -> Bool { + static func ==(lhs: ToolbarComponent, rhs: ToolbarComponent) -> Bool { + if lhs.bottomInset != rhs.bottomInset { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.leftItem != rhs.leftItem { + return false + } + if lhs.rightItem != rhs.rightItem { + return false + } + if lhs.centerItem != rhs.centerItem { + return false + } + return true } static var body: Body { + let background = Child(Rectangle.self) let leftItem = Child(environment: Empty.self) let rightItem = Child(environment: Empty.self) let centerItem = Child(environment: Empty.self) return { context in var availableWidth = context.availableSize.width - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 16.0 + context.component.sideInset + + let contentHeight: CGFloat = 44.0 + let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset) + + let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition) let leftItem = context.component.leftItem.flatMap { leftItemComponent in return leftItem.update( component: leftItemComponent, - availableSize: CGSize(width: availableWidth, height: context.availableSize.height), + availableSize: CGSize(width: availableWidth, height: contentHeight), transition: context.transition ) } @@ -47,7 +404,7 @@ private final class NavigationBarComponent: CombinedComponent { let rightItem = context.component.rightItem.flatMap { rightItemComponent in return rightItem.update( component: rightItemComponent, - availableSize: CGSize(width: availableWidth, height: context.availableSize.height), + availableSize: CGSize(width: availableWidth, height: contentHeight), transition: context.transition ) } @@ -58,7 +415,7 @@ private final class NavigationBarComponent: CombinedComponent { let centerItem = context.component.centerItem.flatMap { centerItemComponent in return centerItem.update( component: centerItemComponent, - availableSize: CGSize(width: availableWidth, height: context.availableSize.height), + availableSize: CGSize(width: availableWidth, height: contentHeight), transition: context.transition ) } @@ -66,10 +423,14 @@ private final class NavigationBarComponent: CombinedComponent { availableWidth -= centerItem.size.width } + context.add(background + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + var centerLeftInset = sideInset if let leftItem = leftItem { context.add(leftItem - .position(CGPoint(x: sideInset + leftItem.size.width / 2.0, y: context.availableSize.height / 2.0)) + .position(CGPoint(x: sideInset + leftItem.size.width / 2.0, y: contentHeight / 2.0)) ) centerLeftInset += leftItem.size.width + 4.0 } @@ -77,7 +438,7 @@ private final class NavigationBarComponent: CombinedComponent { var centerRightInset = sideInset if let rightItem = rightItem { context.add(rightItem - .position(CGPoint(x: context.availableSize.width - sideInset - rightItem.size.width / 2.0, y: context.availableSize.height / 2.0)) + .position(CGPoint(x: context.availableSize.width - sideInset - rightItem.size.width / 2.0, y: contentHeight / 2.0)) ) centerRightInset += rightItem.size.width + 4.0 } @@ -85,16 +446,21 @@ private final class NavigationBarComponent: CombinedComponent { let maxCenterInset = max(centerLeftInset, centerRightInset) if let centerItem = centerItem { context.add(centerItem - .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: context.availableSize.height / 2.0)) + .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: contentHeight / 2.0)) ) } - return context.availableSize + return size } } } public final class MediaStreamComponent: CombinedComponent { + struct OriginInfo: Equatable { + var title: String + var memberCount: Int + } + public typealias EnvironmentType = ViewControllerComponentContainer.Environment public let call: PresentationGroupCallImpl @@ -116,10 +482,24 @@ public final class MediaStreamComponent: CombinedComponent { private(set) var hasVideo: Bool = false private var stateDisposable: Disposable? + private var infoDisposable: Disposable? + + private(set) var originInfo: OriginInfo? + + private(set) var displayUI: Bool = true + var dismissOffset: CGFloat = 0.0 + + let isPictureInPictureSupported: Bool init(call: PresentationGroupCallImpl) { self.call = call + if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { + self.isPictureInPictureSupported = true + } else { + self.isPictureInPictureSupported = false + } + super.init() self.stateDisposable = (call.state @@ -140,11 +520,44 @@ public final class MediaStreamComponent: CombinedComponent { strongSelf.updated(transition: .immediate) }) + let peerId = call.peerId + let callPeer = call.accountContext.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + + self.infoDisposable = (combineLatest(queue: .mainQueue(), call.members, callPeer) + |> deliverOnMainQueue).start(next: { [weak self] members, callPeer in + guard let strongSelf = self, let members = members, let callPeer = callPeer else { + return + } + + let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount) + if strongSelf.originInfo != originInfo { + strongSelf.originInfo = originInfo + strongSelf.updated(transition: .immediate) + } + }) + let _ = call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false).start() } deinit { self.stateDisposable?.dispose() + self.infoDisposable?.dispose() + } + + func toggleDisplayUI() { + self.displayUI = !self.displayUI + self.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .easeInOut))) + } + + func updateDismissOffset(value: CGFloat, interactive: Bool) { + self.dismissOffset = value + if interactive { + self.updated(transition: .immediate) + } else { + self.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } } } @@ -156,9 +569,15 @@ public final class MediaStreamComponent: CombinedComponent { let background = Child(Rectangle.self) let video = Child(MediaStreamVideoComponent.self) let navigationBar = Child(NavigationBarComponent.self) + let toolbar = Child(ToolbarComponent.self) + + let activatePictureInPicture = StoredActionSlot(Action.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + if !environment.isVisible { + context.state.dismissOffset = 0.0 + } let background = background.update( component: Rectangle(color: .black), @@ -166,44 +585,190 @@ public final class MediaStreamComponent: CombinedComponent { transition: context.transition ) + let call = context.component.call + let controller = environment.controller + let video = Condition(context.state.hasVideo) { return video.update( component: MediaStreamVideoComponent( - call: context.component.call + call: context.component.call, + activatePictureInPicture: activatePictureInPicture, + bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in + guard let call = call else { + completed() + return + } + + call.accountContext.sharedContext.mainWindow?.inCallNavigate?() + + completed() + } ), - availableSize: CGSize(width: context.availableSize.width, height: floor(context.availableSize.width * 9.0 / 16.0)), + availableSize: context.availableSize, transition: context.transition ) } - let call = context.component.call + var navigationRightItems: [AnyComponentWithIdentity] = [] + if context.state.isPictureInPictureSupported, let _ = video { + navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Media Gallery/PictureInPictureButton", + tintColor: .white + )), + action: { + activatePictureInPicture.invoke(Action { + guard let controller = controller() as? MediaStreamComponentController else { + return + } + controller.dismiss(closing: false, manual: true) + }) + } + ).minSize(CGSize(width: 44.0, height: 44.0))))) + } + + /*let whiteColor = UIColor(white: 1.0, alpha: 1.0) + navigationRightItems.append(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( + color: .white, + size: CGSize(width: 22.0, height: 22.0), + width: 1.5 + ))), + AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent( + animation: LottieAnimationComponent.Animation( + name: "anim_profilemore", + colors: [ + "Point 2.Group 1.Fill 1": whiteColor, + "Point 3.Group 1.Fill 1": whiteColor, + "Point 1.Group 1.Fill 1": whiteColor + ], + loop: true + ), + size: CGSize(width: 22.0, height: 22.0) + ))), + ])), + action: { + activatePictureInPicture.invoke(Action { + guard let controller = controller() as? MediaStreamComponentController else { + return + } + controller.dismiss(closing: false, manual: true) + }) + } + ).minSize(CGSize(width: 44.0, height: 44.0)))))*/ + + //TODO:localize let navigationBar = navigationBar.update( component: NavigationBarComponent( + topInset: environment.statusBarHeight, + sideInset: environment.safeInsets.left, leftItem: AnyComponent(Button( - content: AnyComponent(Text(text: "Leave", font: Font.regular(17.0), color: .white)), - insets: UIEdgeInsets(), + content: AnyComponent(NavigationBackButtonComponent(text: environment.strings.Common_Back, color: .white)), action: { [weak call] in let _ = call?.leave(terminateIfPossible: false) }) ), - rightItem: nil, + rightItems: navigationRightItems, centerItem: AnyComponent(Text(text: "Live Stream", font: Font.semibold(17.0), color: .white)) ), - availableSize: CGSize(width: context.availableSize.width, height: 44.0), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: context.transition ) + let isLandscape = context.availableSize.width > context.availableSize.height + + var infoItem: AnyComponent? + if let originInfo = context.state.originInfo { + let memberCountString: String + if originInfo.memberCount == 1 { + memberCountString = "1 viewer" + } else { + memberCountString = "\(originInfo.memberCount) viewers" + } + infoItem = AnyComponent(OriginInfoComponent( + title: originInfo.title, + subtitle: memberCountString + )) + } + + let toolbar = toolbar.update( + component: ToolbarComponent( + bottomInset: environment.safeInsets.bottom, + sideInset: environment.safeInsets.left, + leftItem: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Accessory Panels/MessageSelectionForward", + tintColor: .white + )), + action: { + guard let controller = controller() as? MediaStreamComponentController else { + return + } + controller.presentShare() + } + ).minSize(CGSize(width: 44.0, height: 44.0))), + rightItem: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: isLandscape ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen", + tintColor: .white + )), + action: { + if let controller = controller() as? MediaStreamComponentController { + controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) + } + } + ).minSize(CGSize(width: 44.0, height: 44.0))), + centerItem: infoItem + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: context.transition + ) + + let state = context.state + let height = context.availableSize.height context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + .gesture(.tap { [weak state] in + guard let state = state else { + return + } + state.toggleDisplayUI() + }) + .gesture(.pan { [weak state] panState in + guard let state = state else { + return + } + switch panState { + case .began: + break + case let .updated(offset): + state.updateDismissOffset(value: offset.y, interactive: true) + case let .ended(velocity): + if abs(velocity.y) > 200.0 { + state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false) + (controller() as? MediaStreamComponentController)?.dismiss(closing: false, manual: true) + } else { + state.updateDismissOffset(value: 0.0, interactive: false) + } + } + }) ) if let video = video { context.add(video - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))) + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0 + context.state.dismissOffset)) + ) } context.add(navigationBar - .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.statusBarHeight + navigationBar.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height / 2.0)) + .opacity(context.state.displayUI ? 1.0 : 0.0) + ) + + context.add(toolbar + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) + .opacity(context.state.displayUI ? 1.0 : 0.0) ) return context.availableSize @@ -219,24 +784,41 @@ public final class MediaStreamComponentController: ViewControllerComponentContai public var onViewDidAppear: (() -> Void)? public var onViewDidDisappear: (() -> Void)? + private var initialOrientation: UIInterfaceOrientation? + public init(call: PresentationGroupCall) { self.call = call - super.init(MediaStreamComponent(call: call as! PresentationGroupCallImpl)) + super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl)) self.statusBar.statusBarStyle = .White + self.view.disablesInteractiveModalDismiss = true } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + if let initialOrientation = self.initialOrientation { + self.call.accountContext.sharedContext.applicationBindings.forceOrientation(initialOrientation) + } + } + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) DispatchQueue.main.async { self.onViewDidAppear?() } + + self.view.layer.allowsGroupOpacity = true + self.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.view.layer.allowsGroupOpacity = false + }) } override public func viewDidDisappear(_ animated: Bool) { @@ -248,6 +830,115 @@ public final class MediaStreamComponentController: ViewControllerComponentContai } public func dismiss(closing: Bool, manual: Bool) { - self.dismiss(animated: true, completion: nil) + self.dismiss(completion: nil) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.view.layer.allowsGroupOpacity = true + self.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in + guard let strongSelf = self else { + completion?() + return + } + strongSelf.view.layer.allowsGroupOpacity = false + strongSelf.dismissImpl(completion: completion) + }) + } + + private func dismissImpl(completion: (() -> Void)? = nil) { + super.dismiss(completion: completion) + } + + func updateOrientation(orientation: UIInterfaceOrientation) { + if self.initialOrientation == nil { + self.initialOrientation = orientation == .portrait ? .landscapeRight : .portrait + } else if self.initialOrientation == orientation { + self.initialOrientation = nil + } + self.call.accountContext.sharedContext.applicationBindings.forceOrientation(orientation) + } + + func presentShare() { + let formatSendTitle: (String) -> String = { string in + var string = string + if string.contains("[") && string.contains("]") { + if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") { + string.removeSubrange(startIndex ... endIndex) + } + } else { + string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.")) + } + return string + } + + let _ = (combineLatest(self.call.accountContext.account.postbox.loadedPeerWithId(self.call.peerId), self.call.state |> take(1)) + |> deliverOnMainQueue).start(next: { [weak self] peer, callState in + if let strongSelf = self { + var maybeInviteLinks: GroupCallInviteLinks? = nil + + if let peer = peer as? TelegramChannel, let addressName = peer.addressName { + maybeInviteLinks = GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)", speakerLink: nil) + } + + guard let inviteLinks = maybeInviteLinks else { + return + } + + let presentationData = strongSelf.call.accountContext.sharedContext.currentPresentationData.with { $0 } + + var segmentedValues: [ShareControllerSegmentedValue]? + if let speakerLink = inviteLinks.speakerLink { + segmentedValues = [ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in + return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count))) + }), ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in + return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count))) + })] + } + let shareController = ShareController(context: strongSelf.call.accountContext, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkColorPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.call.accountContext.account.postbox.transaction { transaction -> [Peer] in + var peers: [Peer] = [] + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + peers.append(peer) + } + } + return peers + } |> deliverOnMainQueue).start(next: { peers in + if let strongSelf = self { + let presentationData = strongSelf.call.accountContext.sharedContext.currentPresentationData.with { $0 } + + let text: String + var isSavedMessages = false + if peers.count == 1, let peer = peers.first { + isSavedMessages = peer.id == strongSelf.call.accountContext.account.peerId + let peerName = peer.id == strongSelf.call.accountContext.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.call.accountContext.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(firstPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.call.accountContext.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(secondPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.VoiceChat_ForwardTooltip_ManyChats(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + }) + } + } + shareController.actionCompleted = { + if let strongSelf = self { + let presentationData = strongSelf.call.accountContext.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + } + strongSelf.present(shareController, in: .window(.root)) + } + }) } } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index a3cecd7d0a..9b05313e40 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -2,12 +2,17 @@ import Foundation import UIKit import ComponentFlow import AccountContext +import AVKit final class MediaStreamVideoComponent: Component { let call: PresentationGroupCallImpl + let activatePictureInPicture: ActionSlot> + let bringBackControllerForPictureInPictureDeactivation: (@escaping () -> Void) -> Void - init(call: PresentationGroupCallImpl) { + init(call: PresentationGroupCallImpl, activatePictureInPicture: ActionSlot>, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void) { self.call = call + self.activatePictureInPicture = activatePictureInPicture + self.bringBackControllerForPictureInPictureDeactivation = bringBackControllerForPictureInPictureDeactivation } public static func ==(lhs: MediaStreamVideoComponent, rhs: MediaStreamVideoComponent) -> Bool { @@ -18,34 +23,145 @@ final class MediaStreamVideoComponent: Component { return true } - public final class View: UIView { + public final class State: ComponentState { + override init() { + super.init() + } + } + + public func makeState() -> State { + return State() + } + + public final class View: UIView, AVPictureInPictureControllerDelegate, AVPictureInPictureSampleBufferPlaybackDelegate { private let videoRenderingContext = VideoRenderingContext() private var videoView: VideoRenderingView? + private let blurTintView: UIView + private var videoBlurView: VideoRenderingView? - func update(component: MediaStreamVideoComponent, availableSize: CGSize, transition: Transition) -> CGSize { + private var pictureInPictureController: AVPictureInPictureController? + + private var component: MediaStreamVideoComponent? + + override init(frame: CGRect) { + self.blurTintView = UIView() + self.blurTintView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + super.init(frame: frame) + + self.isUserInteractionEnabled = false + self.clipsToBounds = true + + self.addSubview(self.blurTintView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { if self.videoView == nil { if let input = component.call.video(endpointId: "unified") { - if let videoView = self.videoRenderingContext.makeView(input: input, blur: false) { + if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { + self.videoBlurView = videoBlurView + self.insertSubview(videoBlurView, belowSubview: self.blurTintView) + } + + if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) { self.videoView = videoView self.addSubview(videoView) + + if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), let sampleBufferVideoView = videoView as? SampleBufferVideoRenderingView { + let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: sampleBufferVideoView.sampleBufferLayer, playbackDelegate: self)) + self.pictureInPictureController = pictureInPictureController + + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true + pictureInPictureController.requiresLinearPlayback = true + pictureInPictureController.delegate = self + } + + videoView.setOnOrientationUpdated { [weak state] _, _ in + state?.updated(transition: .immediate) + } + videoView.setOnFirstFrameReceived { [weak state] _ in + state?.updated(transition: .immediate) + } } } } if let videoView = self.videoView { videoView.updateIsEnabled(true) - videoView.frame = CGRect(origin: CGPoint(), size: availableSize) + var aspect = videoView.getAspect() + if aspect <= 0.01 { + aspect = 3.0 / 4.0 + } + + let videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(availableSize) + let blurredVideoSize = videoSize.aspectFilled(availableSize) + + transition.withAnimation(.none).setFrame(view: videoView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize), completion: nil) + + if let videoBlurView = self.videoBlurView { + videoBlurView.updateIsEnabled(true) + transition.withAnimation(.none).setFrame(view: videoBlurView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - blurredVideoSize.width) / 2.0), y: floor((availableSize.height - blurredVideoSize.height) / 2.0)), size: blurredVideoSize), completion: nil) + } + } + + self.component = component + + component.activatePictureInPicture.connect { [weak self] completion in + guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else { + return + } + + pictureInPictureController.startPictureInPicture() + + completion(Void()) } return availableSize } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) { + } + + public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { + return CMTimeRange(start: .zero, duration: .positiveInfinity) + } + + public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + return false + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { + completionHandler() + } + + public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + return false + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + guard let component = self.component else { + completionHandler(false) + return + } + + component.bringBackControllerForPictureInPictureDeactivation { + completionHandler(true) + } + } } public func makeView() -> View { - return View() + return View(frame: CGRect()) } - public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/TelegramCallsUI/Sources/Components/ViewControllerComponent.swift b/submodules/TelegramCallsUI/Sources/Components/ViewControllerComponent.swift index fe4d4e34a5..18bce06c63 100644 --- a/submodules/TelegramCallsUI/Sources/Components/ViewControllerComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/ViewControllerComponent.swift @@ -3,6 +3,8 @@ import UIKit import ComponentFlow import Display import SwiftSignalKit +import TelegramPresentationData +import AccountContext public extension Transition.Animation.Curve { init(_ curve: ContainedViewLayoutTransitionCurve) { @@ -36,34 +38,59 @@ open class ViewControllerComponentContainer: ViewController { public final class Environment: Equatable { public let statusBarHeight: CGFloat public let safeInsets: UIEdgeInsets + public let isVisible: Bool + public let strings: PresentationStrings + public let controller: () -> ViewController? public init( statusBarHeight: CGFloat, - safeInsets: UIEdgeInsets + safeInsets: UIEdgeInsets, + isVisible: Bool, + strings: PresentationStrings, + controller: @escaping () -> ViewController? ) { self.statusBarHeight = statusBarHeight self.safeInsets = safeInsets + self.isVisible = isVisible + self.strings = strings + self.controller = controller } public static func ==(lhs: Environment, rhs: Environment) -> Bool { + if lhs === rhs { + return true + } + if lhs.statusBarHeight != rhs.statusBarHeight { return false } if lhs.safeInsets != rhs.safeInsets { return false } + if lhs.isVisible != rhs.isVisible { + return false + } + if lhs.strings !== rhs.strings { + return false + } return true } } private final class Node: ViewControllerTracingNode { + private var presentationData: PresentationData private weak var controller: ViewControllerComponentContainer? private let component: AnyComponent private let hostView: ComponentHostView - init(controller: ViewControllerComponentContainer, component: AnyComponent) { + private var currentIsVisible: Bool = false + private var currentLayout: ContainerViewLayout? + + init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.controller = controller self.component = component @@ -74,20 +101,39 @@ open class ViewControllerComponentContainer: ViewController { self.view.addSubview(self.hostView) } - func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: Transition) { + self.currentLayout = layout + let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, - safeInsets: layout.intrinsicInsets + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.intrinsicInsets.left + layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.intrinsicInsets.right + layout.safeInsets.right), + isVisible: self.currentIsVisible, + strings: self.presentationData.strings, + controller: { [weak self] in + return self?.controller + } ) let _ = self.hostView.update( - transition: Transition(transition), + transition: transition, component: self.component, environment: { environment }, containerSize: layout.size ) - transition.updateFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) + } + + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(currentLayout, transition: .immediate) } } @@ -95,9 +141,11 @@ open class ViewControllerComponentContainer: ViewController { return self.displayNode as! Node } + private let context: AccountContext private let component: AnyComponent - public init(_ component: C) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + public init(context: AccountContext, component: C) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.context = context self.component = AnyComponent(component) super.init(navigationBarPresentationData: nil) @@ -108,14 +156,26 @@ open class ViewControllerComponentContainer: ViewController { } override open func loadDisplayNode() { - self.displayNode = Node(controller: self, component: self.component) + self.displayNode = Node(context: self.context, controller: self, component: self.component) self.displayNodeDidLoad() } + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout, transition: transition) + self.node.containerLayoutUpdated(layout, transition: Transition(transition)) } } diff --git a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift index 67333e7fcf..dd9e682b90 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift @@ -527,9 +527,11 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { var joinText = self.strings.VoiceChat_PanelJoin var title = self.strings.VoiceChat_Title var isChannel = false - if let currentData = self.currentData, currentData.isChannel { - title = self.strings.VoiceChatChannel_Title - isChannel = true + if let currentData = self.currentData { + if currentData.isChannel || currentData.info.isStream { + title = self.strings.VoiceChatChannel_Title + isChannel = true + } } var text = self.currentText var isScheduled = false diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index ebddea535c..de1d054cd1 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -679,7 +679,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.audioOutputStatePromise.set(.single(([], .speaker))) } - self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, activateImmediately: true, manualActivate: { [weak self] control in + self.audioSessionDisposable = audioSession.push(audioSessionType: self.isStream ? .play : .voiceCall, activateImmediately: true, manualActivate: { [weak self] control in Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateSessionState(internalState: strongSelf.internalState, audioSessionControl: control) @@ -725,13 +725,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if value { if let audioSessionControl = strongSelf.audioSessionControl { - //let audioSessionActive: Signal - if let callKitIntegration = strongSelf.callKitIntegration { + if !strongSelf.isStream, let callKitIntegration = strongSelf.callKitIntegration { _ = callKitIntegration.audioSessionActive |> filter { $0 } |> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { subscriber in - /*if let strongSelf = self, let _ = strongSelf.audioSessionControl { - }*/ subscriber.putNext(true) subscriber.putCompletion() return EmptyDisposable @@ -1355,11 +1352,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.internalStatePromise.set(.single(internalState)) if let audioSessionControl = audioSessionControl, previousControl == nil { - switch self.currentSelectedAudioOutputValue { - case .speaker: - audioSessionControl.setOutputMode(.custom(self.currentSelectedAudioOutputValue)) - default: - break + if self.isStream { + audioSessionControl.setOutputMode(.system) + } else { + switch self.currentSelectedAudioOutputValue { + case .speaker: + audioSessionControl.setOutputMode(.custom(self.currentSelectedAudioOutputValue)) + default: + break + } } audioSessionControl.setup(synchronous: false) } @@ -1427,7 +1428,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.requestCall(movingFromBroadcastToRtc: false) } } - }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264") + }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264") self.genericCallContext = genericCallContext self.stateVersionValue += 1 @@ -1521,10 +1522,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.currentConnectionMode = .rtc strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false) strongSelf.genericCallContext?.setJoinResponse(payload: clientParams) - case let .broadcast(isExternalStream): + case .broadcast: strongSelf.currentConnectionMode = .broadcast - strongSelf.genericCallContext?.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: isExternalStream)) - strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: isExternalStream) + strongSelf.genericCallContext?.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream)) + strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: callInfo.isStream) } strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl) @@ -1893,7 +1894,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - if let chatPeer = chatPeer, !participants.contains(where: { $0.peer.id == chatPeer.id }) { + /*if let chatPeer = chatPeer, !participants.contains(where: { $0.peer.id == chatPeer.id }) { participants.append(GroupCallParticipantsContext.Participant( peer: chatPeer, ssrc: 100, @@ -1915,7 +1916,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { joinedVideo: false )) participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) - } + }*/ var otherParticipantsWithVideo = 0 var videoWatchingParticipants = 0 @@ -2712,7 +2713,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.hasScreencast = true - let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, preferX264: false) + let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, disableAudioInput: true, preferX264: false) self.screencastCallContext = screencastCallContext self.screencastJoinDisposable.set((screencastCallContext.joinPayload @@ -2823,7 +2824,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { )) if let audioSessionControl = self.audioSessionControl { - audioSessionControl.setOutputMode(.custom(output)) + if self.isStream { + audioSessionControl.setOutputMode(.system) + } else { + audioSessionControl.setOutputMode(.custom(output)) + } } } diff --git a/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift b/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift index a21f3859a0..9e45116cf0 100644 --- a/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift +++ b/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift @@ -108,7 +108,7 @@ final class SampleBufferVideoRenderingView: UIView, VideoRenderingView { return AVSampleBufferDisplayLayer.self } - private var sampleBufferLayer: AVSampleBufferDisplayLayer { + var sampleBufferLayer: AVSampleBufferDisplayLayer { return self.layer as! AVSampleBufferDisplayLayer } diff --git a/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift b/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift index b4d66797fd..9c043ac454 100644 --- a/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift +++ b/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift @@ -33,14 +33,18 @@ class VideoRenderingContext { } #endif - func makeView(input: Signal, blur: Bool) -> VideoRenderingView? { + func makeView(input: Signal, blur: Bool, forceSampleBufferDisplayLayer: Bool = false) -> VideoRenderingView? { #if targetEnvironment(simulator) if blur { + #if DEBUG + return SampleBufferVideoRenderingView(input: input) + #else return nil + #endif } return SampleBufferVideoRenderingView(input: input) #else - if #available(iOS 13.0, *) { + if #available(iOS 13.0, *), !forceSampleBufferDisplayLayer { return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: blur) } else { if blur { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 005adfa30a..3d0cf437f7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -2534,7 +2534,7 @@ func _internal_getVideoBroadcastPart(dataSource: AudioBroadcastDataSource, callI status: .notReady, responseTimestamp: responseTimestamp )) - } else if error.errorDescription == "TIME_INVALID" || error.errorDescription == "TIME_TOO_SMALL" { + } else if error.errorDescription == "TIME_INVALID" || error.errorDescription == "TIME_TOO_SMALL" || error.errorDescription.hasSuffix("_CHANNEL_INVALID") { return .single(GetAudioBroadcastPartResult( status: .resyncNeeded, responseTimestamp: responseTimestamp diff --git a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift index 42f36a207b..bd8dfa9707 100644 --- a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift @@ -52,7 +52,7 @@ final class BlurredRoundedRectangle: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -166,7 +166,7 @@ final class RadialProgressComponent: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -308,7 +308,7 @@ final class CheckComponent: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -558,7 +558,7 @@ final class AvatarComponent: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -656,7 +656,7 @@ private final class WallpaperBlurComponent: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } @@ -979,7 +979,7 @@ final class OverscrollContentsComponent: Component { return View() } - func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 85fadf42b0..32209dcc3b 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -382,7 +382,7 @@ public final class OngoingGroupCallContext { private let broadcastPartsSource = Atomic(value: nil) - init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, preferX264: Bool) { + init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool) { self.queue = queue var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? @@ -488,6 +488,7 @@ public final class OngoingGroupCallContext { outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32, videoContentType: _videoContentType, enableNoiseSuppression: enableNoiseSuppression, + disableAudioInput: disableAudioInput, preferX264: preferX264 ) @@ -900,10 +901,10 @@ public final class OngoingGroupCallContext { } } - public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, preferX264: Bool) { + public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, preferX264: preferX264) + return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264) }) } diff --git a/submodules/TgVoipWebrtc/BUILD b/submodules/TgVoipWebrtc/BUILD index e45b78171b..842991e710 100644 --- a/submodules/TgVoipWebrtc/BUILD +++ b/submodules/TgVoipWebrtc/BUILD @@ -8,8 +8,6 @@ config_setting( optimization_flags = select({ ":debug_build": [ - "-O2", - "-DNDEBUG", ], "//conditions:default": ["-DNDEBUG"], }) diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 3e777135af..bc7dbff2cb 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -350,6 +350,7 @@ typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) { outgoingAudioBitrateKbit:(int32_t)outgoingAudioBitrateKbit videoContentType:(OngoingGroupCallVideoContentType)videoContentType enableNoiseSuppression:(bool)enableNoiseSuppression + disableAudioInput:(bool)disableAudioInput preferX264:(bool)preferX264; - (void)stop; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 11a638ddc4..1b862efe8a 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -1364,6 +1364,7 @@ private: outgoingAudioBitrateKbit:(int32_t)outgoingAudioBitrateKbit videoContentType:(OngoingGroupCallVideoContentType)videoContentType enableNoiseSuppression:(bool)enableNoiseSuppression + disableAudioInput:(bool)disableAudioInput preferX264:(bool)preferX264 { self = [super init]; if (self != nil) { @@ -1532,6 +1533,7 @@ private: }, .outgoingAudioBitrateKbit = outgoingAudioBitrateKbit, .disableOutgoingAudioProcessing = disableOutgoingAudioProcessing, + .disableAudioInput = disableAudioInput, .videoContentType = _videoContentType, .videoCodecPreferences = videoCodecPreferences, .initialEnableNoiseSuppression = enableNoiseSuppression, diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index a5ae22266f..25177c8e73 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit a5ae22266f4113c60dd21bc405371b98af0359cd +Subproject commit 25177c8e7354c5e3ac892ad3561c26915eb24d4e diff --git a/third-party/webrtc/BUILD b/third-party/webrtc/BUILD index a7cb262c36..2390d8efe5 100644 --- a/third-party/webrtc/BUILD +++ b/third-party/webrtc/BUILD @@ -10,8 +10,6 @@ config_setting( optimization_flags = select({ ":debug_build": [ - "-O2", - "-DNDEBUG", ], "//conditions:default": ["-DNDEBUG"], })