mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Video chats
This commit is contained in:
parent
b1c3aeda82
commit
f21f7f66f7
@ -335,14 +335,14 @@ public enum PresentationGroupCallTone {
|
||||
case recordingStarted
|
||||
}
|
||||
|
||||
public struct PresentationGroupCallRequestedVideo {
|
||||
public struct PresentationGroupCallRequestedVideo: Equatable {
|
||||
public enum Quality {
|
||||
case thumbnail
|
||||
case medium
|
||||
case full
|
||||
}
|
||||
|
||||
public struct SsrcGroup {
|
||||
public struct SsrcGroup: Equatable {
|
||||
public var semantics: String
|
||||
public var ssrcs: [UInt32]
|
||||
}
|
||||
@ -441,6 +441,7 @@ public protocol PresentationGroupCall: AnyObject {
|
||||
func updateDefaultParticipantsAreMuted(isMuted: Bool)
|
||||
func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool)
|
||||
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
|
||||
func setSuspendVideoChannelRequests(_ value: Bool)
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
|
||||
func playTone(_ tone: PresentationGroupCallTone)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
public final class RoundedRectangle: Component {
|
||||
public enum GradientDirection: Equatable {
|
||||
@ -117,3 +118,136 @@ public final class RoundedRectangle: Component {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class FilledRoundedRectangleComponent: Component {
|
||||
public let color: UIColor
|
||||
public let cornerRadius: CGFloat
|
||||
public let smoothCorners: Bool
|
||||
|
||||
public init(
|
||||
color: UIColor,
|
||||
cornerRadius: CGFloat,
|
||||
smoothCorners: Bool
|
||||
) {
|
||||
self.color = color
|
||||
self.cornerRadius = cornerRadius
|
||||
self.smoothCorners = smoothCorners
|
||||
}
|
||||
|
||||
public static func ==(lhs: FilledRoundedRectangleComponent, rhs: FilledRoundedRectangleComponent) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
if lhs.smoothCorners != rhs.smoothCorners {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIImageView {
|
||||
private var component: FilledRoundedRectangleComponent?
|
||||
|
||||
private var currentCornerRadius: CGFloat?
|
||||
private var cornerImage: UIImage?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func applyStaticCornerRadius() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let cornerRadius = self.currentCornerRadius else {
|
||||
return
|
||||
}
|
||||
if cornerRadius == 0.0 {
|
||||
if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 {
|
||||
} else {
|
||||
self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
} else {
|
||||
if component.smoothCorners {
|
||||
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
|
||||
if let cornerImage = self.cornerImage, cornerImage.size == size {
|
||||
} else {
|
||||
self.cornerImage = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5).withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
} else {
|
||||
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
|
||||
if let cornerImage = self.cornerImage, cornerImage.size == size {
|
||||
} else {
|
||||
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.image = self.cornerImage
|
||||
self.clipsToBounds = false
|
||||
self.backgroundColor = nil
|
||||
self.layer.cornerRadius = 0.0
|
||||
}
|
||||
|
||||
func update(component: FilledRoundedRectangleComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
transition.setTintColor(view: self, color: component.color)
|
||||
|
||||
if self.currentCornerRadius != component.cornerRadius {
|
||||
let previousCornerRadius = self.currentCornerRadius
|
||||
self.currentCornerRadius = component.cornerRadius
|
||||
if transition.animation.isImmediate {
|
||||
self.applyStaticCornerRadius()
|
||||
} else {
|
||||
self.image = nil
|
||||
self.clipsToBounds = true
|
||||
self.backgroundColor = component.color
|
||||
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
|
||||
self.layer.cornerRadius = previousCornerRadius
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
if component.smoothCorners {
|
||||
self.layer.cornerCurve = .continuous
|
||||
} else {
|
||||
self.layer.cornerCurve = .circular
|
||||
}
|
||||
|
||||
}
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius, completion: { [weak self] completed in
|
||||
guard let self, completed else {
|
||||
return
|
||||
}
|
||||
self.applyStaticCornerRadius()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
@ -664,6 +664,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private var ssrcMapping: [UInt32: SsrcMapping] = [:]
|
||||
|
||||
private var requestedVideoChannels: [OngoingGroupCallContext.VideoChannel] = []
|
||||
private var suspendVideoChannelRequests: Bool = false
|
||||
private var pendingVideoSubscribers = Bag<(String, MetaDisposable, (OngoingGroupCallContext.VideoFrameData) -> Void)>()
|
||||
|
||||
private var summaryInfoState = Promise<SummaryInfoState?>(nil)
|
||||
@ -1699,7 +1700,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.genericCallContext = genericCallContext
|
||||
self.stateVersionValue += 1
|
||||
|
||||
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
|
||||
genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels)
|
||||
self.connectPendingVideoSubscribers()
|
||||
}
|
||||
|
||||
@ -3090,11 +3091,21 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
maxQuality: mappedMaxQuality
|
||||
)
|
||||
}
|
||||
if let genericCallContext = self.genericCallContext {
|
||||
if let genericCallContext = self.genericCallContext, !self.suspendVideoChannelRequests {
|
||||
genericCallContext.setRequestedVideoChannels(self.requestedVideoChannels)
|
||||
}
|
||||
}
|
||||
|
||||
public func setSuspendVideoChannelRequests(_ value: Bool) {
|
||||
if self.suspendVideoChannelRequests != value {
|
||||
self.suspendVideoChannelRequests = value
|
||||
|
||||
if let genericCallContext = self.genericCallContext {
|
||||
genericCallContext.setRequestedVideoChannels(self.suspendVideoChannelRequests ? [] : self.requestedVideoChannels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||
guard self.currentSelectedAudioOutputValue != output else {
|
||||
return
|
||||
|
@ -34,13 +34,16 @@ final class VideoChatActionButtonComponent: Component {
|
||||
|
||||
let content: Content
|
||||
let microphoneState: MicrophoneState
|
||||
let isCollapsed: Bool
|
||||
|
||||
init(
|
||||
content: Content,
|
||||
microphoneState: MicrophoneState
|
||||
microphoneState: MicrophoneState,
|
||||
isCollapsed: Bool
|
||||
) {
|
||||
self.content = content
|
||||
self.microphoneState = microphoneState
|
||||
self.isCollapsed = isCollapsed
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatActionButtonComponent, rhs: VideoChatActionButtonComponent) -> Bool {
|
||||
@ -50,6 +53,9 @@ final class VideoChatActionButtonComponent: Component {
|
||||
if lhs.microphoneState != rhs.microphoneState {
|
||||
return false
|
||||
}
|
||||
if lhs.isCollapsed != rhs.isCollapsed {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -80,6 +86,8 @@ final class VideoChatActionButtonComponent: Component {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
|
||||
|
||||
let titleText: String
|
||||
let backgroundColor: UIColor
|
||||
let iconDiameter: CGFloat
|
||||
@ -138,9 +146,10 @@ final class VideoChatActionButtonComponent: Component {
|
||||
|
||||
let _ = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(RoundedRectangle(
|
||||
component: AnyComponent(FilledRoundedRectangleComponent(
|
||||
color: backgroundColor,
|
||||
cornerRadius: nil
|
||||
cornerRadius: size.width * 0.5,
|
||||
smoothCorners: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
@ -159,6 +168,7 @@ final class VideoChatActionButtonComponent: Component {
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
|
@ -14,17 +14,23 @@ final class VideoChatMicButtonComponent: Component {
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let isCollapsed: Bool
|
||||
|
||||
init(
|
||||
content: Content
|
||||
content: Content,
|
||||
isCollapsed: Bool
|
||||
) {
|
||||
self.content = content
|
||||
self.isCollapsed = isCollapsed
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.isCollapsed != rhs.isCollapsed {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -52,6 +58,8 @@ final class VideoChatMicButtonComponent: Component {
|
||||
|
||||
self.component = component
|
||||
|
||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
|
||||
|
||||
let titleText: String
|
||||
let backgroundColor: UIColor
|
||||
switch component.content {
|
||||
@ -79,9 +87,10 @@ final class VideoChatMicButtonComponent: Component {
|
||||
|
||||
let _ = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(RoundedRectangle(
|
||||
component: AnyComponent(FilledRoundedRectangleComponent(
|
||||
color: backgroundColor,
|
||||
cornerRadius: nil
|
||||
cornerRadius: size.width * 0.5,
|
||||
smoothCorners: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
@ -100,6 +109,7 @@ final class VideoChatMicButtonComponent: Component {
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
@ -118,7 +128,9 @@ final class VideoChatMicButtonComponent: Component {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
transition.setPosition(view: iconView, position: iconFrame.center)
|
||||
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
transition.setScale(view: iconView, scale: component.isCollapsed ? ((iconSize.width - 24.0) / iconSize.width) : 1.0)
|
||||
}
|
||||
|
||||
return size
|
||||
|
@ -12,18 +12,32 @@ import AccountContext
|
||||
import SwiftSignalKit
|
||||
|
||||
final class VideoChatParticipantVideoComponent: Component {
|
||||
struct ExpandedState: Equatable {
|
||||
var isPinned: Bool
|
||||
|
||||
init(isPinned: Bool) {
|
||||
self.isPinned = isPinned
|
||||
}
|
||||
}
|
||||
|
||||
let call: PresentationGroupCall
|
||||
let participant: GroupCallParticipantsContext.Participant
|
||||
let isPresentation: Bool
|
||||
let expandedState: ExpandedState?
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
participant: GroupCallParticipantsContext.Participant,
|
||||
isPresentation: Bool
|
||||
isPresentation: Bool,
|
||||
expandedState: ExpandedState?,
|
||||
action: (() -> Void)?
|
||||
) {
|
||||
self.call = call
|
||||
self.participant = participant
|
||||
self.isPresentation = isPresentation
|
||||
self.expandedState = expandedState
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool {
|
||||
@ -33,6 +47,12 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
if lhs.isPresentation != rhs.isPresentation {
|
||||
return false
|
||||
}
|
||||
if lhs.expandedState != rhs.expandedState {
|
||||
return false
|
||||
}
|
||||
if (lhs.action == nil) != (rhs.action == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -46,9 +66,9 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
final class View: HighlightTrackingButton {
|
||||
private var component: VideoChatParticipantVideoComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
@ -62,8 +82,11 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
//TODO:release optimize
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 10.0
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -74,6 +97,13 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
self.videoDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component, let action = component.action else {
|
||||
return
|
||||
}
|
||||
action()
|
||||
}
|
||||
|
||||
func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -81,7 +111,7 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.componentState = state
|
||||
|
||||
let nameColor = component.participant.peer.nameColor ?? .blue
|
||||
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
|
||||
@ -146,14 +176,14 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
if self.videoSpec != videoSpec {
|
||||
self.videoSpec = videoSpec
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
self.componentState?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.videoSpec != nil {
|
||||
self.videoSpec = nil
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
self.componentState?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,36 +12,93 @@ import TelegramPresentationData
|
||||
import PeerListItemComponent
|
||||
|
||||
final class VideoChatParticipantsComponent: Component {
|
||||
struct VideoParticipantKey: Hashable {
|
||||
var id: EnginePeer.Id
|
||||
var isPresentation: Bool
|
||||
|
||||
init(id: EnginePeer.Id, isPresentation: Bool) {
|
||||
self.id = id
|
||||
self.isPresentation = isPresentation
|
||||
}
|
||||
}
|
||||
|
||||
final class ExpandedVideoState: Equatable {
|
||||
let mainParticipant: VideoParticipantKey
|
||||
let isMainParticipantPinned: Bool
|
||||
|
||||
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool) {
|
||||
self.mainParticipant = mainParticipant
|
||||
self.isMainParticipantPinned = isMainParticipantPinned
|
||||
}
|
||||
|
||||
static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.mainParticipant != rhs.mainParticipant {
|
||||
return false
|
||||
}
|
||||
if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let call: PresentationGroupCall
|
||||
let members: PresentationGroupCallMembers?
|
||||
let expandedVideoState: ExpandedVideoState?
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let collapsedContainerInsets: UIEdgeInsets
|
||||
let expandedContainerInsets: UIEdgeInsets
|
||||
let sideInset: CGFloat
|
||||
let updateMainParticipant: (VideoParticipantKey?) -> Void
|
||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
members: PresentationGroupCallMembers?,
|
||||
expandedVideoState: ExpandedVideoState?,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat
|
||||
collapsedContainerInsets: UIEdgeInsets,
|
||||
expandedContainerInsets: UIEdgeInsets,
|
||||
sideInset: CGFloat,
|
||||
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
|
||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.call = call
|
||||
self.members = members
|
||||
self.expandedVideoState = expandedVideoState
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.collapsedContainerInsets = collapsedContainerInsets
|
||||
self.expandedContainerInsets = expandedContainerInsets
|
||||
self.sideInset = sideInset
|
||||
self.updateMainParticipant = updateMainParticipant
|
||||
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
||||
if lhs.members != rhs.members {
|
||||
return false
|
||||
}
|
||||
if lhs.expandedVideoState != rhs.expandedVideoState {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets {
|
||||
return false
|
||||
}
|
||||
if lhs.expandedContainerInsets != rhs.expandedContainerInsets {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
@ -116,6 +173,20 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpandedGrid {
|
||||
let containerSize: CGSize
|
||||
let containerInsets: UIEdgeInsets
|
||||
|
||||
init(containerSize: CGSize, containerInsets: UIEdgeInsets) {
|
||||
self.containerSize = containerSize
|
||||
self.containerInsets = containerInsets
|
||||
}
|
||||
|
||||
func itemContainerFrame() -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top - containerInsets.bottom))
|
||||
}
|
||||
}
|
||||
|
||||
struct List {
|
||||
let containerSize: CGSize
|
||||
let sideInset: CGFloat
|
||||
@ -168,19 +239,24 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let containerSize: CGSize
|
||||
let sideInset: CGFloat
|
||||
let grid: Grid
|
||||
let expandedGrid: ExpandedGrid
|
||||
let list: List
|
||||
let spacing: CGFloat
|
||||
let gridOffsetY: CGFloat
|
||||
let listOffsetY: CGFloat
|
||||
|
||||
init(containerSize: CGSize, sideInset: CGFloat, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
||||
init(containerSize: CGSize, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
||||
self.containerSize = containerSize
|
||||
self.sideInset = sideInset
|
||||
|
||||
self.grid = Grid(containerSize: containerSize, sideInset: sideInset, itemCount: gridItemCount)
|
||||
self.list = List(containerSize: containerSize, sideInset: sideInset, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
||||
self.grid = Grid(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
|
||||
self.expandedGrid = ExpandedGrid(containerSize: containerSize, containerInsets: expandedContainerInsets)
|
||||
self.list = List(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
||||
self.spacing = 4.0
|
||||
|
||||
var listOffsetY: CGFloat = 0.0
|
||||
self.gridOffsetY = collapsedContainerInsets.top
|
||||
|
||||
var listOffsetY: CGFloat = self.gridOffsetY
|
||||
if self.grid.itemCount != 0 {
|
||||
listOffsetY += self.grid.contentHeight()
|
||||
listOffsetY += self.spacing
|
||||
@ -199,42 +275,40 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
func visibleGridItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
|
||||
return self.grid.visibleItemRange(for: rect)
|
||||
return self.grid.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.gridOffsetY))
|
||||
}
|
||||
|
||||
func gridItemFrame(at index: Int) -> CGRect {
|
||||
return self.grid.frame(at: index)
|
||||
}
|
||||
|
||||
func gridItemContainerFrame() -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight()))
|
||||
}
|
||||
|
||||
func visibleListItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
|
||||
return self.list.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.listOffsetY))
|
||||
}
|
||||
|
||||
func listItemFrame(at index: Int) -> CGRect {
|
||||
return self.list.frame(at: index).offsetBy(dx: 0.0, dy: self.listOffsetY)
|
||||
return self.list.frame(at: index)
|
||||
}
|
||||
|
||||
func listItemContainerFrame() -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight()))
|
||||
}
|
||||
|
||||
func listTrailingItemFrame() -> CGRect {
|
||||
return self.list.trailingItemFrame().offsetBy(dx: 0.0, dy: self.listOffsetY)
|
||||
return self.list.trailingItemFrame()
|
||||
}
|
||||
}
|
||||
|
||||
private final class VideoParticipant: Equatable {
|
||||
struct Key: Hashable {
|
||||
var id: EnginePeer.Id
|
||||
var isPresentation: Bool
|
||||
|
||||
init(id: EnginePeer.Id, isPresentation: Bool) {
|
||||
self.id = id
|
||||
self.isPresentation = isPresentation
|
||||
}
|
||||
}
|
||||
|
||||
let participant: GroupCallParticipantsContext.Participant
|
||||
let isPresentation: Bool
|
||||
|
||||
var key: Key {
|
||||
return Key(id: self.participant.peer.id, isPresentation: self.isPresentation)
|
||||
var key: VideoParticipantKey {
|
||||
return VideoParticipantKey(id: self.participant.peer.id, isPresentation: self.isPresentation)
|
||||
}
|
||||
|
||||
init(participant: GroupCallParticipantsContext.Participant, isPresentation: Bool) {
|
||||
@ -252,8 +326,19 @@ final class VideoChatParticipantsComponent: Component {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class GridItem {
|
||||
let key: VideoParticipantKey
|
||||
let view = ComponentView<Empty>()
|
||||
var isCollapsing: Bool = false
|
||||
|
||||
init(key: VideoParticipantKey) {
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let scollViewClippingContainer: UIView
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private var component: VideoChatParticipantsComponent?
|
||||
@ -267,16 +352,35 @@ final class VideoChatParticipantsComponent: Component {
|
||||
private let measureListItemView = ComponentView<Empty>()
|
||||
private let inviteListItemView = ComponentView<Empty>()
|
||||
|
||||
private var gridItemViews: [VideoParticipant.Key: ComponentView<Empty>] = [:]
|
||||
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
|
||||
private let gridItemViewContainer: UIView
|
||||
|
||||
private let expandedGridItemContainer: UIView
|
||||
private var expandedGridItemView: GridItem?
|
||||
|
||||
private var listItemViews: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
private let listItemViewContainer: UIView
|
||||
private let listItemsBackround = ComponentView<Empty>()
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var appliedGridIsEmpty: Bool = true
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scollViewClippingContainer = UIView()
|
||||
self.scollViewClippingContainer.clipsToBounds = true
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
|
||||
self.gridItemViewContainer = UIView()
|
||||
self.gridItemViewContainer.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
|
||||
self.listItemViewContainer = UIView()
|
||||
self.listItemViewContainer.clipsToBounds = true
|
||||
|
||||
self.expandedGridItemContainer = UIView()
|
||||
self.expandedGridItemContainer.clipsToBounds = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delaysContentTouches = false
|
||||
@ -294,13 +398,38 @@ final class VideoChatParticipantsComponent: Component {
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
self.scollViewClippingContainer.addSubview(self.scrollView)
|
||||
self.addSubview(self.scollViewClippingContainer)
|
||||
|
||||
self.scrollView.addSubview(self.listItemViewContainer)
|
||||
self.scrollView.addSubview(self.gridItemViewContainer)
|
||||
self.addSubview(self.expandedGridItemContainer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let component = self.component else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if component.expandedVideoState != nil {
|
||||
if let result = self.expandedGridItemContainer.hitTest(self.convert(point, to: self.expandedGridItemContainer), with: event) {
|
||||
return result
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
} else {
|
||||
if let result = self.scollViewClippingContainer.hitTest(self.convert(point, to: self.scollViewClippingContainer), with: event) {
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
@ -312,51 +441,158 @@ final class VideoChatParticipantsComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
var validGridItemIds: [VideoParticipant.Key] = []
|
||||
let gridWasEmpty = self.appliedGridIsEmpty
|
||||
let gridIsEmpty = self.gridParticipants.isEmpty
|
||||
self.appliedGridIsEmpty = gridIsEmpty
|
||||
|
||||
var expandedGridItemContainerFrame: CGRect
|
||||
if component.expandedVideoState != nil {
|
||||
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
|
||||
} else {
|
||||
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
|
||||
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
|
||||
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
|
||||
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
|
||||
}
|
||||
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - component.collapsedContainerInsets.bottom {
|
||||
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - component.collapsedContainerInsets.bottom)
|
||||
}
|
||||
if expandedGridItemContainerFrame.size.height < 0.0 {
|
||||
expandedGridItemContainerFrame.size.height = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
let commonGridItemTransition: ComponentTransition = (gridIsEmpty == gridWasEmpty) ? transition : .immediate
|
||||
|
||||
var validGridItemIds: [VideoParticipantKey] = []
|
||||
var validGridItemIndices: [Int] = []
|
||||
|
||||
let visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
||||
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
||||
for i in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
||||
let videoParticipant = self.gridParticipants[i]
|
||||
validGridItemIds.append(videoParticipant.key)
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ComponentView<Empty>
|
||||
if let current = self.gridItemViews[videoParticipant.key] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ComponentView()
|
||||
self.gridItemViews[videoParticipant.key] = itemView
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.gridItemFrame(at: i)
|
||||
|
||||
let _ = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(VideoChatParticipantVideoComponent(
|
||||
call: component.call,
|
||||
participant: videoParticipant.participant,
|
||||
isPresentation: videoParticipant.isPresentation
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
||||
let videoParticipant = self.gridParticipants[index]
|
||||
let videoParticipantKey = videoParticipant.key
|
||||
validGridItemIds.append(videoParticipantKey)
|
||||
validGridItemIndices.append(index)
|
||||
}
|
||||
}
|
||||
if let expandedVideoState = component.expandedVideoState {
|
||||
if !validGridItemIds.contains(expandedVideoState.mainParticipant), let index = self.gridParticipants.firstIndex(where: { $0.key == expandedVideoState.mainParticipant }) {
|
||||
validGridItemIds.append(expandedVideoState.mainParticipant)
|
||||
validGridItemIndices.append(index)
|
||||
}
|
||||
}
|
||||
|
||||
for index in validGridItemIndices {
|
||||
let videoParticipant = self.gridParticipants[index]
|
||||
let videoParticipantKey = videoParticipant.key
|
||||
validGridItemIds.append(videoParticipantKey)
|
||||
|
||||
var itemTransition = commonGridItemTransition
|
||||
let itemView: GridItem
|
||||
if let current = self.gridItemViews[videoParticipantKey] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = GridItem(key: videoParticipant.key)
|
||||
self.gridItemViews[videoParticipantKey] = itemView
|
||||
}
|
||||
|
||||
var expandedItemState: VideoChatParticipantVideoComponent.ExpandedState?
|
||||
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
|
||||
expandedItemState = VideoChatParticipantVideoComponent.ExpandedState(isPinned: expandedVideoState.isMainParticipantPinned)
|
||||
}
|
||||
|
||||
let itemFrame: CGRect
|
||||
if expandedItemState != nil {
|
||||
itemFrame = CGRect(origin: CGPoint(), size: itemLayout.expandedGrid.itemContainerFrame().size)
|
||||
} else {
|
||||
itemFrame = itemLayout.gridItemFrame(at: index)
|
||||
}
|
||||
|
||||
let _ = itemView.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(VideoChatParticipantVideoComponent(
|
||||
call: component.call,
|
||||
participant: videoParticipant.participant,
|
||||
isPresentation: videoParticipant.isPresentation,
|
||||
expandedState: expandedItemState,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if component.expandedVideoState?.mainParticipant == videoParticipantKey {
|
||||
component.updateMainParticipant(nil)
|
||||
} else {
|
||||
component.updateMainParticipant(videoParticipantKey)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
if let itemComponentView = itemView.view.view {
|
||||
if itemComponentView.superview == nil {
|
||||
if expandedItemState != nil {
|
||||
self.expandedGridItemContainer.addSubview(itemComponentView)
|
||||
} else {
|
||||
self.gridItemViewContainer.addSubview(itemComponentView)
|
||||
}
|
||||
|
||||
itemComponentView.frame = itemFrame
|
||||
|
||||
if !commonGridItemTransition.animation.isImmediate {
|
||||
commonGridItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
|
||||
}
|
||||
if !transition.animation.isImmediate {
|
||||
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
}
|
||||
} else if expandedItemState != nil && itemComponentView.superview != self.expandedGridItemContainer {
|
||||
let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer)
|
||||
itemComponentView.center = fromFrame.center
|
||||
self.expandedGridItemContainer.addSubview(itemComponentView)
|
||||
} else if expandedItemState == nil && itemComponentView.superview != self.gridItemViewContainer {
|
||||
if !itemView.isCollapsing {
|
||||
itemView.isCollapsing = true
|
||||
let targetLocalItemFrame = itemLayout.gridItemFrame(at: index)
|
||||
var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self)
|
||||
targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY
|
||||
targetItemFrame.origin.x -= expandedGridItemContainerFrame.minX
|
||||
commonGridItemTransition.setPosition(view: itemComponentView, position: targetItemFrame.center)
|
||||
commonGridItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: targetItemFrame.size), completion: { [weak self, weak itemView, weak itemComponentView] _ in
|
||||
guard let self, let itemView, let itemComponentView else {
|
||||
return
|
||||
}
|
||||
itemView.isCollapsing = false
|
||||
self.gridItemViewContainer.addSubview(itemComponentView)
|
||||
itemComponentView.center = targetLocalItemFrame.center
|
||||
itemComponentView.bounds = CGRect(origin: CGPoint(), size: targetLocalItemFrame.size)
|
||||
})
|
||||
}
|
||||
}
|
||||
if !itemView.isCollapsing {
|
||||
commonGridItemTransition.setPosition(view: itemComponentView, position: itemFrame.center)
|
||||
commonGridItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removedGridItemIds: [VideoParticipant.Key] = []
|
||||
var removedGridItemIds: [VideoParticipantKey] = []
|
||||
for (itemId, itemView) in self.gridItemViews {
|
||||
if !validGridItemIds.contains(itemId) {
|
||||
removedGridItemIds.append(itemId)
|
||||
|
||||
if let itemComponentView = itemView.view {
|
||||
itemComponentView.removeFromSuperview()
|
||||
if let itemComponentView = itemView.view.view {
|
||||
if !transition.animation.isImmediate {
|
||||
if commonGridItemTransition.animation.isImmediate == transition.animation.isImmediate {
|
||||
transition.setScale(view: itemComponentView, scale: 0.001)
|
||||
}
|
||||
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in
|
||||
itemComponentView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemComponentView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -418,9 +654,16 @@ final class VideoChatParticipantsComponent: Component {
|
||||
)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
itemComponentView.clipsToBounds = true
|
||||
|
||||
self.listItemViewContainer.addSubview(itemComponentView)
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
itemComponentView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: 0.0))
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
transition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -431,7 +674,13 @@ final class VideoChatParticipantsComponent: Component {
|
||||
removedListItemIds.append(itemId)
|
||||
|
||||
if let itemComponentView = itemView.view {
|
||||
itemComponentView.removeFromSuperview()
|
||||
if !transition.animation.isImmediate {
|
||||
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in
|
||||
itemComponentView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemComponentView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -448,11 +697,18 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
self.listItemViewContainer.addSubview(itemComponentView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
|
||||
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
|
||||
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
|
||||
transition.setBounds(view: self.gridItemViewContainer, bounds: CGRect(origin: CGPoint(), size: itemLayout.gridItemContainerFrame().size))
|
||||
transition.setFrame(view: self.listItemViewContainer, frame: itemLayout.listItemContainerFrame())
|
||||
|
||||
transition.setFrame(view: self.expandedGridItemContainer, frame: expandedGridItemContainerFrame)
|
||||
}
|
||||
|
||||
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
@ -533,6 +789,8 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: availableSize,
|
||||
sideInset: component.sideInset,
|
||||
collapsedContainerInsets: component.collapsedContainerInsets,
|
||||
expandedContainerInsets: component.expandedContainerInsets,
|
||||
gridItemCount: gridParticipants.count,
|
||||
listItemCount: listParticipants.count,
|
||||
listItemHeight: measureListItemSize.height,
|
||||
@ -549,10 +807,10 @@ final class VideoChatParticipantsComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: itemLayout.list.contentHeight())
|
||||
)
|
||||
let listItemsBackroundFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: itemLayout.listOffsetY), size: listItemsBackroundSize)
|
||||
let listItemsBackroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackroundSize)
|
||||
if let listItemsBackroundView = self.listItemsBackround.view {
|
||||
if listItemsBackroundView.superview == nil {
|
||||
self.scrollView.addSubview(listItemsBackroundView)
|
||||
self.listItemViewContainer.addSubview(listItemsBackroundView)
|
||||
}
|
||||
transition.setFrame(view: listItemsBackroundView, frame: listItemsBackroundFrame)
|
||||
}
|
||||
@ -560,18 +818,38 @@ final class VideoChatParticipantsComponent: Component {
|
||||
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
|
||||
if let members = component.members {
|
||||
for participant in members.participants {
|
||||
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
|
||||
requestedVideo.append(videoChannel)
|
||||
var maxVideoQuality: PresentationGroupCallRequestedVideo.Quality = .medium
|
||||
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant.id == participant.peer.id, !expandedVideoState.mainParticipant.isPresentation {
|
||||
maxVideoQuality = .full
|
||||
}
|
||||
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
|
||||
requestedVideo.append(videoChannel)
|
||||
|
||||
var maxPresentationQuality: PresentationGroupCallRequestedVideo.Quality = .medium
|
||||
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant.id == participant.peer.id, expandedVideoState.mainParticipant.isPresentation {
|
||||
maxPresentationQuality = .full
|
||||
}
|
||||
|
||||
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
|
||||
if !requestedVideo.contains(videoChannel) {
|
||||
requestedVideo.append(videoChannel)
|
||||
}
|
||||
}
|
||||
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) {
|
||||
if !requestedVideo.contains(videoChannel) {
|
||||
requestedVideo.append(videoChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
|
||||
|
||||
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: component.collapsedContainerInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.collapsedContainerInsets.top - component.collapsedContainerInsets.bottom))
|
||||
transition.setPosition(view: self.scollViewClippingContainer, position: scrollClippingFrame.center)
|
||||
transition.setBounds(view: self.scollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
if self.scrollView.bounds.size != availableSize {
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight())
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
|
@ -73,6 +73,11 @@ private final class VideoChatScreenComponent: Component {
|
||||
private var members: PresentationGroupCallMembers?
|
||||
private var membersDisposable: Disposable?
|
||||
|
||||
private let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private var applicationStateDisposable: Disposable?
|
||||
|
||||
private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
@ -96,6 +101,7 @@ private final class VideoChatScreenComponent: Component {
|
||||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
self.membersDisposable?.dispose()
|
||||
self.applicationStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -272,7 +278,7 @@ private final class VideoChatScreenComponent: Component {
|
||||
self.members = members
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -290,8 +296,22 @@ private final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.applicationStateDisposable = (combineLatest(queue: .mainQueue(),
|
||||
component.call.accountContext.sharedContext.applicationBindings.applicationIsActive,
|
||||
self.isPresentedValue.get()
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] applicationIsActive, isPresented in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let suspendVideoChannelRequests = !applicationIsActive || !isPresented
|
||||
component.call.setSuspendVideoChannelRequests(suspendVideoChannelRequests)
|
||||
})
|
||||
}
|
||||
|
||||
self.isPresentedValue.set(environment.isVisible)
|
||||
|
||||
self.component = component
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
@ -419,7 +439,7 @@ private final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
|
||||
let actionButtonDiameter: CGFloat = 56.0
|
||||
let microphoneButtonDiameter: CGFloat = 116.0
|
||||
let microphoneButtonDiameter: CGFloat = self.expandedParticipantsVideoState == nil ? 116.0 : actionButtonDiameter
|
||||
|
||||
let maxActionMicrophoneButtonSpacing: CGFloat = 38.0
|
||||
let buttonsSideInset: CGFloat = 42.0
|
||||
@ -428,23 +448,60 @@ private final class VideoChatScreenComponent: Component {
|
||||
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
|
||||
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
|
||||
|
||||
let microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - microphoneButtonDiameter), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
|
||||
let microphoneButtonFrame: CGRect
|
||||
if self.expandedParticipantsVideoState == nil {
|
||||
microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - microphoneButtonDiameter), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
|
||||
} else {
|
||||
microphoneButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - microphoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - microphoneButtonDiameter - 12.0), size: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter))
|
||||
}
|
||||
|
||||
let participantsClippingY: CGFloat
|
||||
if self.expandedParticipantsVideoState == nil {
|
||||
participantsClippingY = microphoneButtonFrame.minY
|
||||
} else {
|
||||
participantsClippingY = microphoneButtonFrame.minY - 24.0
|
||||
}
|
||||
|
||||
let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
|
||||
let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter))
|
||||
|
||||
let participantsSize = self.participants.update(
|
||||
let participantsSize = availableSize
|
||||
let participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - participantsClippingY, right: environment.safeInsets.right)
|
||||
let participantsExpandedInsets = UIEdgeInsets(top: environment.statusBarHeight, left: environment.safeInsets.left, bottom: availableSize.height - participantsClippingY, right: environment.safeInsets.right)
|
||||
|
||||
let _ = self.participants.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatParticipantsComponent(
|
||||
call: component.call,
|
||||
members: self.members,
|
||||
expandedVideoState: self.expandedParticipantsVideoState,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
sideInset: sideInset
|
||||
collapsedContainerInsets: participantsCollapsedInsets,
|
||||
expandedContainerInsets: participantsExpandedInsets,
|
||||
sideInset: sideInset,
|
||||
updateMainParticipant: { [weak self] key in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let key {
|
||||
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key {
|
||||
return
|
||||
}
|
||||
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
} else if self.expandedParticipantsVideoState != nil {
|
||||
self.expandedParticipantsVideoState = nil
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
updateIsMainParticipantPinned: { isPinned in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: microphoneButtonFrame.minY - navigationHeight)
|
||||
containerSize: participantsSize
|
||||
)
|
||||
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: participantsSize)
|
||||
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: participantsSize)
|
||||
if let participantsView = self.participants.view {
|
||||
if participantsView.superview == nil {
|
||||
self.containerView.addSubview(participantsView)
|
||||
@ -477,7 +534,8 @@ private final class VideoChatScreenComponent: Component {
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(VideoChatMicButtonComponent(
|
||||
content: micButtonContent
|
||||
content: micButtonContent,
|
||||
isCollapsed: self.expandedParticipantsVideoState != nil
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
@ -514,7 +572,8 @@ private final class VideoChatScreenComponent: Component {
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(VideoChatActionButtonComponent(
|
||||
content: .video(isActive: false),
|
||||
microphoneState: actionButtonMicrophoneState
|
||||
microphoneState: actionButtonMicrophoneState,
|
||||
isCollapsed: self.expandedParticipantsVideoState != nil
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
@ -541,7 +600,8 @@ private final class VideoChatScreenComponent: Component {
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(VideoChatActionButtonComponent(
|
||||
content: .leave,
|
||||
microphoneState: actionButtonMicrophoneState
|
||||
microphoneState: actionButtonMicrophoneState,
|
||||
isCollapsed: self.expandedParticipantsVideoState != nil
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
@ -665,7 +725,9 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
||||
self.idleTimerExtensionDisposable = self.call.accountContext.sharedContext.applicationBindings.pushIdleTimerExtension()
|
||||
}
|
||||
|
||||
self.onViewDidAppear?()
|
||||
DispatchQueue.main.async {
|
||||
self.onViewDidAppear?()
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidDisappear(_ animated: Bool) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user