Stream improvements

This commit is contained in:
Ali 2022-02-26 02:22:10 +04:00
parent 6fb5008c0f
commit 089642a0bb
35 changed files with 1227 additions and 309 deletions

View File

@ -614,7 +614,7 @@ private final class DayComponent: Component {
return View()
}
func update(view: View, availableSize: CGSize, environment: Environment<DayEnvironment>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DayEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}

View File

@ -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

View File

@ -617,7 +617,7 @@ public extension CombinedComponent {
return UIView()
}
func update(view: View, availableSize: CGSize, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, 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<Arguments>(_ argumentsType: Arguments.Type) -> ActionSlot<Arguments> {
return ActionSlot<Arguments>()
}
}

View File

@ -118,7 +118,7 @@ public protocol Component: _TypeErasedComponent, Equatable {
func makeView() -> View
func makeState() -> State
func update(view: View, availableSize: CGSize, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, 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<EnvironmentType>, 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<EnvironmentType>, transition: transition)
}
func _isEqual(to other: _TypeErasedComponent) -> Bool {

View File

@ -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 {

View File

@ -1,68 +1,126 @@
import Foundation
import UIKit
public final class Button: CombinedComponent, Equatable {
public final class Button: Component {
public let content: AnyComponent<Empty>
public let insets: UIEdgeInsets
public let minSize: CGSize?
public let action: () -> Void
public init(
convenience public init(
content: AnyComponent<Empty>,
insets: UIEdgeInsets,
action: @escaping () -> Void
) {
self.init(
content: content,
minSize: nil,
action: action
)
}
private init(
content: AnyComponent<Empty>,
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<Empty>
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<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -44,7 +44,7 @@ public final class Image: Component {
return View()
}
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}

View File

@ -25,7 +25,7 @@ public final class Rectangle: Component {
return true
}
public func update(view: UIView, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: UIView, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
var size = availableSize
if let width = self.width {
size.width = min(size.width, width)

View File

@ -95,7 +95,7 @@ public final class Text: Component {
return View()
}
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize)
}
}

View File

@ -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))

View File

@ -17,7 +17,7 @@ private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? {
return nil
}
public final class ComponentHostView<EnvironmentType>: UIView {
public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
private var currentComponent: AnyComponent<EnvironmentType>?
private var currentContainerSize: CGSize?
private var currentSize: CGSize?
@ -33,19 +33,12 @@ public final class ComponentHostView<EnvironmentType>: UIView {
}
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, 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<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, containerSize: CGSize) -> CGSize {
private func _update(transition: Transition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize {
precondition(!self.isUpdating)
self.isUpdating = true
@ -72,6 +65,20 @@ public final class ComponentHostView<EnvironmentType>: 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<EnvironmentType>: UIView {
}
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
preconditionFailure()
} as () -> Environment<EnvironmentType>, updateEnvironment: false, containerSize: containerSize)
} as () -> Environment<EnvironmentType>, updateEnvironment: false, forceUpdate: true, containerSize: containerSize)
}
let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition)

View File

@ -1,134 +1,3 @@
import Foundation
import UIKit
public final class RootHostView<EnvironmentType: Equatable>: UIViewController {
private let content: AnyComponent<(NavigationLayout, EnvironmentType)>
private var keyboardWillChangeFrameObserver: NSObjectProtocol?
private var inputHeight: CGFloat = 0.0
private let environment: Environment<EnvironmentType>
private var componentView: ComponentHostView<(NavigationLayout, EnvironmentType)>
private var scheduledTransition: Transition?
public init(
content: AnyComponent<(NavigationLayout, EnvironmentType)>,
@EnvironmentBuilder environment: () -> Environment<EnvironmentType>
) {
self.content = content
self.environment = Environment<EnvironmentType>()
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<EnvironmentType>) {
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)
}
}

View File

@ -0,0 +1,28 @@
import Foundation
public final class Action<Arguments> {
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<Arguments> {
private var target: ((Arguments) -> Void)?
init() {
}
public func connect(_ target: @escaping (Arguments) -> Void) {
self.target = target
}
public func invoke(_ arguments: Arguments) {
self.target?(arguments)
}
}

View File

@ -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<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -107,7 +107,7 @@ public final class ProgressIndicatorComponent: Component {
return View()
}
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -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

View File

@ -64,7 +64,7 @@ public final class MultilineText: Component {
return View()
}
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize)
}
}

View File

@ -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

View File

@ -96,6 +96,7 @@ swift_library(
"//submodules/ChatTitleActivityNode:ChatTitleActivityNode",
"//third-party/libyuv:LibYuvBinding",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
],
visibility = [
"//visibility:public",

View File

@ -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<Empty>
private var component: NavigationBackButtonComponent?
override init(frame: CGRect) {
self.arrowView = UIImageView()
self.textView = ComponentHostView<Empty>()
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<Empty>, 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<Empty>, 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<Empty>?
let rightItems: [AnyComponentWithIdentity<Empty>]
let centerItem: AnyComponent<Empty>?
init(
topInset: CGFloat,
sideInset: CGFloat,
leftItem: AnyComponent<Empty>?,
rightItems: [AnyComponentWithIdentity<Empty>],
centerItem: AnyComponent<Empty>?
) {
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<Empty>?
let rightItem: AnyComponent<Empty>?
let centerItem: AnyComponent<Empty>?
init(
bottomInset: CGFloat,
sideInset: CGFloat,
leftItem: AnyComponent<Empty>?,
rightItem: AnyComponent<Empty>?,
centerItem: AnyComponent<Empty>?
) {
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<Void>.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<Empty>] = []
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<Empty>?
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))
}
})
}
}

View File

@ -2,12 +2,17 @@ import Foundation
import UIKit
import ComponentFlow
import AccountContext
import AVKit
final class MediaStreamVideoComponent: Component {
let call: PresentationGroupCallImpl
let activatePictureInPicture: ActionSlot<Action<Void>>
let bringBackControllerForPictureInPictureDeactivation: (@escaping () -> Void) -> Void
init(call: PresentationGroupCallImpl) {
init(call: PresentationGroupCallImpl, activatePictureInPicture: ActionSlot<Action<Void>>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
}
}

View File

@ -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<ViewControllerComponentContainer.Environment>
private let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
init(controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>) {
private var currentIsVisible: Bool = false
private var currentLayout: ContainerViewLayout?
init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>) {
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<ViewControllerComponentContainer.Environment>
public init<C: Component>(_ component: C) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
public init<C: Component>(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))
}
}

View File

@ -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

View File

@ -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<Bool, NoError>
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))
}
}
}

View File

@ -108,7 +108,7 @@ final class SampleBufferVideoRenderingView: UIView, VideoRenderingView {
return AVSampleBufferDisplayLayer.self
}
private var sampleBufferLayer: AVSampleBufferDisplayLayer {
var sampleBufferLayer: AVSampleBufferDisplayLayer {
return self.layer as! AVSampleBufferDisplayLayer
}

View File

@ -33,14 +33,18 @@ class VideoRenderingContext {
}
#endif
func makeView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, blur: Bool) -> VideoRenderingView? {
func makeView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, 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 {

View File

@ -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

View File

@ -52,7 +52,7 @@ final class BlurredRoundedRectangle: Component {
return View()
}
func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -382,7 +382,7 @@ public final class OngoingGroupCallContext {
private let broadcastPartsSource = Atomic<BroadcastPartSource?>(value: nil)
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @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<UInt32>, @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<UInt32>, @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<UInt32>, @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)
})
}

View File

@ -8,8 +8,6 @@ config_setting(
optimization_flags = select({
":debug_build": [
"-O2",
"-DNDEBUG",
],
"//conditions:default": ["-DNDEBUG"],
})

View File

@ -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;

View File

@ -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,

@ -1 +1 @@
Subproject commit a5ae22266f4113c60dd21bc405371b98af0359cd
Subproject commit 25177c8e7354c5e3ac892ad3561c26915eb24d4e

View File

@ -10,8 +10,6 @@ config_setting(
optimization_flags = select({
":debug_build": [
"-O2",
"-DNDEBUG",
],
"//conditions:default": ["-DNDEBUG"],
})