[WIP] Video chat UI

This commit is contained in:
Isaac 2024-09-10 16:01:37 +08:00
parent 1e56520356
commit f203693f89
6 changed files with 540 additions and 252 deletions

View File

@ -134,10 +134,14 @@ final class VideoChatParticipantThumbnailComponent: Component {
if transition.animation.isImmediate {
speakingAlphaTransition = .immediate
} else {
if !wasSpeaking {
speakingAlphaTransition = .easeInOut(duration: 0.1)
if let previousComponent, previousComponent.isSelected == component.isSelected {
if !wasSpeaking {
speakingAlphaTransition = .easeInOut(duration: 0.1)
} else {
speakingAlphaTransition = .easeInOut(duration: 0.25)
}
} else {
speakingAlphaTransition = .easeInOut(duration: 0.25)
speakingAlphaTransition = .immediate
}
}
@ -168,8 +172,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
transition: transition,
component: AnyComponent(VideoChatMuteIconComponent(
color: .white,
isFilled: true,
isMuted: component.participant.muteState != nil
content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking)
)),
environment: {},
containerSize: CGSize(width: 36.0, height: 36.0)
@ -182,8 +185,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
transition.setScale(view: muteStatusView, scale: 0.65)
speakingAlphaTransition.setTintColor(layer: muteStatusView.iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : .white)
}
let titleSize = self.title.update(
@ -203,8 +204,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
speakingAlphaTransition.setTintColor(layer: titleView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : .white)
}
if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
@ -330,11 +329,13 @@ final class VideoChatParticipantThumbnailComponent: Component {
} else {
selectedBorderView = UIImageView()
self.selectedBorderView = selectedBorderView
selectedBorderView.alpha = 0.0
self.addSubview(selectedBorderView)
selectedBorderView.image = View.selectedBorderImage
selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize)
speakingAlphaTransition.setAlpha(view: selectedBorderView, alpha: 1.0)
ComponentTransition.immediate.setTintColor(layer: selectedBorderView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor)
}
} else if let selectedBorderView = self.selectedBorderView {

View File

@ -6,50 +6,49 @@ import MultilineTextComponent
import TelegramPresentationData
import AppBundle
import LottieComponent
import BundleIconComponent
final class VideoChatMuteIconComponent: Component {
enum Content: Equatable {
case mute(isFilled: Bool, isMuted: Bool)
case screenshare
}
let color: UIColor
let isFilled: Bool
let isMuted: Bool
let content: Content
init(
color: UIColor,
isFilled: Bool,
isMuted: Bool
content: Content
) {
self.color = color
self.isFilled = isFilled
self.isMuted = isMuted
self.content = content
}
static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.isFilled != rhs.isFilled {
return false
}
if lhs.isMuted != rhs.isMuted {
if lhs.content != rhs.content {
return false
}
return true
}
final class View: HighlightTrackingButton {
private let icon: VoiceChatMicrophoneNode
private var icon: VoiceChatMicrophoneNode?
private var scheenshareIcon: ComponentView<Empty>?
private var component: VideoChatMuteIconComponent?
private var isUpdating: Bool = false
private var contentImage: UIImage?
var iconView: UIView {
return self.icon.view
var iconView: UIView? {
return self.icon?.view
}
override init(frame: CGRect) {
self.icon = VoiceChatMicrophoneNode()
super.init(frame: frame)
}
@ -65,14 +64,59 @@ final class VideoChatMuteIconComponent: Component {
self.component = component
let animationSize = availableSize
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize))
if self.icon.view.superview == nil {
self.addSubview(self.icon.view)
if case let .mute(isFilled, isMuted) = component.content {
let icon: VoiceChatMicrophoneNode
if let current = self.icon {
icon = current
} else {
icon = VoiceChatMicrophoneNode()
self.icon = icon
self.addSubview(icon.view)
}
let animationSize = availableSize
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: icon.view, frame: animationFrame)
icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate)
} else {
if let icon = self.icon {
self.icon = nil
icon.view.removeFromSuperview()
}
}
if case .screenshare = component.content {
let scheenshareIcon: ComponentView<Empty>
if let current = self.scheenshareIcon {
scheenshareIcon = current
} else {
scheenshareIcon = ComponentView()
self.scheenshareIcon = scheenshareIcon
}
let scheenshareIconSize = scheenshareIcon.update(
transition: transition,
component: AnyComponent(BundleIconComponent(
name: "Call/StatusScreen",
tintColor: component.color
)),
environment: {},
containerSize: availableSize
)
let scheenshareIconFrame = scheenshareIconSize.centered(in: CGRect(origin: CGPoint(), size: availableSize))
if let scheenshareIconView = scheenshareIcon.view {
if scheenshareIconView.superview == nil {
self.addSubview(scheenshareIconView)
}
transition.setPosition(view: scheenshareIconView, position: scheenshareIconFrame.center)
transition.setBounds(view: scheenshareIconView, bounds: CGRect(origin: CGPoint(), size: scheenshareIconFrame.size))
transition.setScale(view: scheenshareIconView, scale: 1.5)
}
} else {
if let scheenshareIcon = self.scheenshareIcon {
self.scheenshareIcon = nil
scheenshareIcon.view?.removeFromSuperview()
}
}
transition.setFrame(view: self.icon.view, frame: animationFrame)
self.icon.update(state: VoiceChatMicrophoneNode.State(muted: component.isMuted, filled: component.isFilled, color: component.color), animated: !transition.animation.isImmediate)
return availableSize
}

View File

@ -61,8 +61,7 @@ final class VideoChatParticipantStatusComponent: Component {
transition: transition,
component: AnyComponent(VideoChatMuteIconComponent(
color: .white,
isFilled: false,
isMuted: component.isMuted && !component.isSpeaking
content: .mute(isFilled: false, isMuted: component.isMuted && !component.isSpeaking)
)),
environment: {},
containerSize: CGSize(width: 36.0, height: 36.0)
@ -80,7 +79,9 @@ final class VideoChatParticipantStatusComponent: Component {
} else {
tintTransition = .immediate
}
tintTransition.setTintColor(layer: muteStatusView.iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : UIColor(white: 1.0, alpha: 0.4))
if let iconView = muteStatusView.iconView {
tintTransition.setTintColor(layer: iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : UIColor(white: 1.0, alpha: 0.4))
}
}
return size

View File

@ -40,7 +40,9 @@ final class VideoChatParticipantVideoComponent: Component {
let isPresentation: Bool
let isSpeaking: Bool
let isExpanded: Bool
let bottomInset: CGFloat
let isUIHidden: Bool
let contentInsets: UIEdgeInsets
let controlInsets: UIEdgeInsets
weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
let action: (() -> Void)?
@ -50,7 +52,9 @@ final class VideoChatParticipantVideoComponent: Component {
isPresentation: Bool,
isSpeaking: Bool,
isExpanded: Bool,
bottomInset: CGFloat,
isUIHidden: Bool,
contentInsets: UIEdgeInsets,
controlInsets: UIEdgeInsets,
rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?,
action: (() -> Void)?
) {
@ -59,7 +63,9 @@ final class VideoChatParticipantVideoComponent: Component {
self.isPresentation = isPresentation
self.isSpeaking = isSpeaking
self.isExpanded = isExpanded
self.bottomInset = bottomInset
self.isUIHidden = isUIHidden
self.contentInsets = contentInsets
self.controlInsets = controlInsets
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
self.action = action
}
@ -77,7 +83,13 @@ final class VideoChatParticipantVideoComponent: Component {
if lhs.isExpanded != rhs.isExpanded {
return false
}
if lhs.bottomInset != rhs.bottomInset {
if lhs.isUIHidden != rhs.isUIHidden {
return false
}
if lhs.contentInsets != rhs.contentInsets {
return false
}
if lhs.controlInsets != rhs.controlInsets {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
@ -153,6 +165,15 @@ final class VideoChatParticipantVideoComponent: Component {
self.component = component
self.componentState = state
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.2)
} else {
alphaTransition = .immediate
}
let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0
let nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4)
@ -210,25 +231,26 @@ final class VideoChatParticipantVideoComponent: Component {
transition: transition,
component: AnyComponent(VideoChatMuteIconComponent(
color: .white,
isFilled: true,
isMuted: component.participant.muteState != nil
content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking)
)),
environment: {},
containerSize: CGSize(width: 36.0, height: 36.0)
)
let muteStatusFrame: CGRect
if component.isExpanded {
muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.bottomInset + 1.0 - muteStatusSize.height), size: muteStatusSize)
muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.controlInsets.bottom + 1.0 - muteStatusSize.height), size: muteStatusSize)
} else {
muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.bottomInset + 3.0 - muteStatusSize.height), size: muteStatusSize)
muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.controlInsets.bottom + 3.0 - muteStatusSize.height), size: muteStatusSize)
}
if let muteStatusView = self.muteStatus.view {
if muteStatusView.superview == nil {
self.addSubview(muteStatusView)
muteStatusView.alpha = controlsAlpha
}
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7)
alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha)
}
let titleSize = self.title.update(
@ -241,18 +263,20 @@ final class VideoChatParticipantVideoComponent: Component {
)
let titleFrame: CGRect
if component.isExpanded {
titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.bottomInset - 8.0 - titleSize.height), size: titleSize)
titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height), size: titleSize)
} else {
titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.bottomInset - 4.0 - titleSize.height), size: titleSize)
titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height), size: titleSize)
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
titleView.alpha = controlsAlpha
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825)
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
}
if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
@ -397,7 +421,7 @@ final class VideoChatParticipantVideoComponent: Component {
let blurredVideoSize = rotatedResolution.aspectFilled(availableSize)
let blurredVideoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - blurredVideoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - blurredVideoSize.height) * 0.5)), size: blurredVideoSize)
let videoResolution = rotatedResolution.aspectFitted(CGSize(width: availableSize.width * 3.0, height: availableSize.height * 3.0))
let videoResolution = rotatedResolution
var rotatedVideoResolution = videoResolution
var rotatedVideoFrame = videoFrame
@ -408,6 +432,7 @@ final class VideoChatParticipantVideoComponent: Component {
rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center)
rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center)
}
rotatedVideoResolution = rotatedVideoResolution.aspectFittedOrSmaller(CGSize(width: rotatedVideoFrame.width * UIScreenScale, height: rotatedVideoFrame.height * UIScreenScale))
transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))

View File

@ -12,21 +12,26 @@ import TelegramPresentationData
import PeerListItemComponent
final class VideoChatParticipantsComponent: Component {
enum LayoutType: Equatable {
struct Horizontal: Equatable {
var rightColumnWidth: CGFloat
var columnSpacing: CGFloat
var isCentered: Bool
struct Layout: Equatable {
struct Column: Equatable {
var width: CGFloat
var insets: UIEdgeInsets
init(rightColumnWidth: CGFloat, columnSpacing: CGFloat, isCentered: Bool) {
self.rightColumnWidth = rightColumnWidth
self.columnSpacing = columnSpacing
self.isCentered = isCentered
init(width: CGFloat, insets: UIEdgeInsets) {
self.width = width
self.insets = insets
}
}
case vertical
case horizontal(Horizontal)
var videoColumn: Column?
var mainColumn: Column
var columnSpacing: CGFloat
init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat) {
self.videoColumn = videoColumn
self.mainColumn = mainColumn
self.columnSpacing = columnSpacing
}
}
final class Participants: Equatable {
@ -75,10 +80,12 @@ final class VideoChatParticipantsComponent: Component {
final class ExpandedVideoState: Equatable {
let mainParticipant: VideoParticipantKey
let isMainParticipantPinned: Bool
let isUIHidden: Bool
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool) {
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool, isUIHidden: Bool) {
self.mainParticipant = mainParticipant
self.isMainParticipantPinned = isMainParticipantPinned
self.isUIHidden = isUIHidden
}
static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool {
@ -91,6 +98,9 @@ final class VideoChatParticipantsComponent: Component {
if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned {
return false
}
if lhs.isUIHidden != rhs.isUIHidden {
return false
}
return true
}
}
@ -101,12 +111,12 @@ final class VideoChatParticipantsComponent: Component {
let expandedVideoState: ExpandedVideoState?
let theme: PresentationTheme
let strings: PresentationStrings
let layoutType: LayoutType
let collapsedContainerInsets: UIEdgeInsets
let expandedContainerInsets: UIEdgeInsets
let sideInset: CGFloat
let layout: Layout
let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let updateMainParticipant: (VideoParticipantKey?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void
init(
call: PresentationGroupCall,
@ -115,12 +125,12 @@ final class VideoChatParticipantsComponent: Component {
expandedVideoState: ExpandedVideoState?,
theme: PresentationTheme,
strings: PresentationStrings,
layoutType: LayoutType,
collapsedContainerInsets: UIEdgeInsets,
expandedContainerInsets: UIEdgeInsets,
sideInset: CGFloat,
layout: Layout,
expandedInsets: UIEdgeInsets,
safeInsets: UIEdgeInsets,
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void
) {
self.call = call
self.participants = participants
@ -128,12 +138,12 @@ final class VideoChatParticipantsComponent: Component {
self.expandedVideoState = expandedVideoState
self.theme = theme
self.strings = strings
self.layoutType = layoutType
self.collapsedContainerInsets = collapsedContainerInsets
self.expandedContainerInsets = expandedContainerInsets
self.sideInset = sideInset
self.layout = layout
self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
self.updateMainParticipant = updateMainParticipant
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
}
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
@ -152,16 +162,13 @@ final class VideoChatParticipantsComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.layoutType != rhs.layoutType {
if lhs.layout != rhs.layout {
return false
}
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets {
if lhs.expandedInsets != rhs.expandedInsets {
return false
}
if lhs.expandedContainerInsets != rhs.expandedContainerInsets {
return false
}
if lhs.sideInset != rhs.sideInset {
if lhs.safeInsets != rhs.safeInsets {
return false
}
return true
@ -178,40 +185,84 @@ final class VideoChatParticipantsComponent: Component {
let containerSize: CGSize
let sideInset: CGFloat
let itemCount: Int
let isDedicatedColumn: Bool
let itemSize: CGSize
let itemSpacing: CGFloat
let lastItemSize: CGFloat
let lastRowItemCount: Int
let lastRowItemSize: CGFloat
let itemsPerRow: Int
let rowCount: Int
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int) {
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, isDedicatedColumn: Bool) {
self.containerSize = containerSize
self.sideInset = sideInset
self.itemCount = itemCount
self.isDedicatedColumn = isDedicatedColumn
let width: CGFloat = containerSize.width - sideInset * 2.0
self.itemSpacing = 4.0
let itemsPerRow: Int
if itemCount == 1 {
itemsPerRow = 1
if isDedicatedColumn {
if itemCount <= 2 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
} else {
itemsPerRow = 2
if itemCount == 1 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
}
self.itemsPerRow = Int(itemsPerRow)
let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow))
let itemHeight = min(180.0, itemWidth)
self.itemSize = CGSize(width: itemWidth, height: itemHeight)
var itemSize = CGSize(width: itemWidth, height: itemHeight)
self.rowCount = itemCount / self.itemsPerRow + ((itemCount % self.itemsPerRow) != 0 ? 1 : 0)
if isDedicatedColumn && itemCount != 0 {
let contentHeight = itemSize.height * CGFloat(self.rowCount) + self.itemSpacing * CGFloat(max(0, self.rowCount - 1))
if contentHeight < containerSize.height {
itemSize.height = (containerSize.height - self.itemSpacing * CGFloat(max(0, self.rowCount - 1))) / CGFloat(self.rowCount)
itemSize.height = floor(itemSize.height)
}
}
self.itemSize = itemSize
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
var lastRowItemCount = itemCount % self.itemsPerRow
if lastRowItemCount == 0 {
lastRowItemCount = self.itemsPerRow
}
self.lastRowItemCount = lastRowItemCount
self.lastRowItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(lastRowItemCount - 1)
}
func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow
let column = index % self.itemsPerRow
let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
let itemWidth: CGFloat
if row == self.rowCount - 1 && column == self.lastRowItemCount - 1 {
itemWidth = self.lastRowItemSize
} else if column == self.itemsPerRow - 1 {
if row == self.rowCount - 1 {
itemWidth = self.lastRowItemSize
} else {
itemWidth = self.lastItemSize
}
} else {
itemWidth = self.itemSize.width
}
let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: itemWidth, height: itemSize.height))
return frame
}
@ -237,21 +288,37 @@ final class VideoChatParticipantsComponent: Component {
struct ExpandedGrid {
let containerSize: CGSize
let layoutType: LayoutType
let containerInsets: UIEdgeInsets
let layout: Layout
let expandedInsets: UIEdgeInsets
let isUIHidden: Bool
init(containerSize: CGSize, layoutType: LayoutType, containerInsets: UIEdgeInsets) {
init(containerSize: CGSize, layout: Layout, expandedInsets: UIEdgeInsets, isUIHidden: Bool) {
self.containerSize = containerSize
self.layoutType = layoutType
self.containerInsets = containerInsets
self.layout = layout
self.expandedInsets = expandedInsets
self.isUIHidden = isUIHidden
}
func itemContainerFrame() -> CGRect {
switch self.layoutType {
case .vertical:
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))
case .horizontal:
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))
let containerInsets: UIEdgeInsets
if self.isUIHidden {
containerInsets = UIEdgeInsets()
} else {
containerInsets = self.expandedInsets
}
if self.layout.videoColumn != nil {
return CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: CGSize(width: self.containerSize.width - containerInsets.left - containerInsets.right, height: self.containerSize.height - containerInsets.top - containerInsets.bottom))
} else {
return CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: CGSize(width: self.containerSize.width - containerInsets.left - containerInsets.right, height: self.containerSize.height - containerInsets.top - containerInsets.bottom))
}
}
func itemContainerInsets() -> UIEdgeInsets {
if self.isUIHidden {
return self.expandedInsets
} else {
return UIEdgeInsets()
}
}
}
@ -306,9 +373,10 @@ final class VideoChatParticipantsComponent: Component {
}
let containerSize: CGSize
let layoutType: LayoutType
let collapsedContainerInsets: UIEdgeInsets
let sideInset: CGFloat
let layout: Layout
let isUIHidden: Bool
let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let grid: Grid
let expandedGrid: ExpandedGrid
let list: List
@ -320,32 +388,41 @@ final class VideoChatParticipantsComponent: Component {
let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: CGRect
init(containerSize: CGSize, layoutType: LayoutType, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
self.containerSize = containerSize
self.layoutType = layoutType
self.collapsedContainerInsets = collapsedContainerInsets
self.sideInset = sideInset
self.layout = layout
self.isUIHidden = isUIHidden
self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
let listWidth: CGFloat = layout.mainColumn.width
let gridWidth: CGFloat
let listWidth: CGFloat
switch layoutType {
case .vertical:
listWidth = containerSize.width - sideInset * 2.0
let gridSideInset: CGFloat
let gridContainerHeight: CGFloat
if let videoColumn = layout.videoColumn {
gridWidth = videoColumn.width
gridSideInset = videoColumn.insets.left
gridContainerHeight = containerSize.height - videoColumn.insets.top - videoColumn.insets.bottom
} else {
gridWidth = listWidth
case let .horizontal(horizontal):
listWidth = horizontal.rightColumnWidth
gridWidth = max(10.0, containerSize.width - sideInset * 2.0 - horizontal.rightColumnWidth - horizontal.columnSpacing)
gridSideInset = layout.mainColumn.insets.left
gridContainerHeight = containerSize.height
}
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layoutType: layoutType, containerInsets: expandedContainerInsets)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.spacing = 4.0
self.gridOffsetY = collapsedContainerInsets.top
if let videoColumn = layout.videoColumn, !isUIHidden {
self.expandedGrid = ExpandedGrid(containerSize: CGSize(width: videoColumn.width + expandedInsets.left, height: containerSize.height), layout: layout, expandedInsets: UIEdgeInsets(top: expandedInsets.top, left: expandedInsets.left, bottom: expandedInsets.bottom, right: 0.0), isUIHidden: isUIHidden)
} else {
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layout: layout, expandedInsets: expandedInsets, isUIHidden: isUIHidden)
}
self.gridOffsetY = layout.mainColumn.insets.top
var listOffsetY: CGFloat = self.gridOffsetY
if case .vertical = layoutType {
if layout.videoColumn == nil {
if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing
@ -353,55 +430,54 @@ final class VideoChatParticipantsComponent: Component {
}
self.listOffsetY = listOffsetY
switch layoutType {
case .vertical:
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.sideInset, y: collapsedContainerInsets.top), size: CGSize(width: containerSize.width - self.sideInset * 2.0, height: containerSize.height - collapsedContainerInsets.top - collapsedContainerInsets.bottom))
self.listFrame = CGRect(origin: CGPoint(), size: containerSize)
if let videoColumn = layout.videoColumn {
let columnsWidth: CGFloat = videoColumn.width + layout.columnSpacing + layout.mainColumn.width
let columnsSideInset: CGFloat = floorToScreenPixels((containerSize.width - columnsWidth) * 0.5)
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: -containerSize.width, y: 0.0), size: containerSize)
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
case let .horizontal(horizontal):
if horizontal.isCentered {
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - horizontal.rightColumnWidth) * 0.5), y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
} else {
self.listFrame = CGRect(origin: CGPoint(x: containerSize.width - self.sideInset - horizontal.rightColumnWidth, y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
var separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height))
var listFrame = CGRect(origin: CGPoint(x: separateVideoGridFrame.maxX + layout.columnSpacing, y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
if isUIHidden {
listFrame.origin.x += columnsSideInset + layout.mainColumn.width
separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height))
}
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - collapsedContainerInsets.top))
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: min(self.sideInset, self.scrollClippingFrame.minX - horizontal.columnSpacing - gridWidth), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height))
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
self.separateVideoGridFrame = separateVideoGridFrame
self.listFrame = listFrame
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: videoColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - videoColumn.insets.top))
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
} else {
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom))
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height))
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
}
}
func contentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY
switch self.layoutType {
case .vertical:
if self.layout.videoColumn == nil {
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
result += self.spacing
}
case .horizontal:
break
}
result += self.list.contentHeight()
result += self.collapsedContainerInsets.bottom
result += self.layout.mainColumn.insets.bottom
result += 24.0
return result
}
func separateVideoGridContentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY
switch self.layoutType {
case .vertical:
break
case .horizontal:
if let videoColumn = self.layout.videoColumn {
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
}
result += videoColumn.insets.bottom
}
result += self.collapsedContainerInsets.bottom
result += 24.0
return result
}
@ -414,11 +490,10 @@ final class VideoChatParticipantsComponent: Component {
}
func gridItemContainerFrame() -> CGRect {
switch self.layoutType {
case .vertical:
return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight()))
case .horizontal:
if let _ = self.layout.videoColumn {
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight()))
} else {
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width, height: self.grid.contentHeight()))
}
}
@ -431,11 +506,10 @@ final class VideoChatParticipantsComponent: Component {
}
func listItemContainerFrame() -> CGRect {
switch self.layoutType {
case .vertical:
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight()))
case .horizontal:
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.listFrame.width, height: self.list.contentHeight()))
if let _ = self.layout.videoColumn {
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.list.contentHeight()))
} else {
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.containerSize.width, height: self.list.contentHeight()))
}
}
@ -620,6 +694,13 @@ final class VideoChatParticipantsComponent: Component {
return
}
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.2)
} else {
alphaTransition = .immediate
}
let gridWasEmpty = self.appliedGridIsEmpty
let gridIsEmpty = self.gridParticipants.isEmpty
self.appliedGridIsEmpty = gridIsEmpty
@ -637,27 +718,26 @@ final class VideoChatParticipantsComponent: Component {
if component.expandedVideoState != nil {
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
} else {
switch itemLayout.layoutType {
case .vertical:
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)
}
case .horizontal:
if let videoColumn = itemLayout.layout.videoColumn {
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: itemLayout.separateVideoScrollClippingFrame.minX, dy: 0.0).offsetBy(dx: 0.0, dy: -self.separateVideoScrollView.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 < videoColumn.insets.top {
expandedGridItemContainerFrame.size.height -= videoColumn.insets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = videoColumn.insets.top
}
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height)
}
} else {
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if expandedGridItemContainerFrame.origin.y < itemLayout.layout.mainColumn.insets.top {
expandedGridItemContainerFrame.size.height -= itemLayout.layout.mainColumn.insets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = itemLayout.layout.mainColumn.insets.top
}
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - itemLayout.layout.mainColumn.insets.bottom {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - itemLayout.layout.mainColumn.insets.bottom)
}
}
if expandedGridItemContainerFrame.size.height < 0.0 {
expandedGridItemContainerFrame.size.height = 0.0
@ -670,10 +750,9 @@ final class VideoChatParticipantsComponent: Component {
var validGridItemIndices: [Int] = []
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
switch itemLayout.layoutType {
case .vertical:
if itemLayout.layout.videoColumn == nil {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
case .horizontal:
} else {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
}
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
@ -707,8 +786,14 @@ final class VideoChatParticipantsComponent: Component {
}
var isItemExpanded = false
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
isItemExpanded = true
var isItemUIHidden = false
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant == videoParticipantKey {
isItemExpanded = true
}
if expandedVideoState.isUIHidden {
isItemUIHidden = true
}
}
var suppressItemExpansionCollapseAnimation = false
@ -734,14 +819,28 @@ final class VideoChatParticipantsComponent: Component {
itemFrame = itemLayout.gridItemFrame(at: index)
}
var itemBottomInset: CGFloat = isItemExpanded ? 96.0 : 0.0
switch itemLayout.layoutType {
case .vertical:
break
case .horizontal:
if isItemExpanded {
itemBottomInset += itemLayout.expandedGrid.containerInsets.bottom
}
let itemContentInsets: UIEdgeInsets
if isItemExpanded {
itemContentInsets = itemLayout.expandedGrid.itemContainerInsets()
} else {
itemContentInsets = UIEdgeInsets()
}
var itemControlInsets: UIEdgeInsets
if isItemExpanded {
itemControlInsets = itemContentInsets
itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0)
} else {
itemControlInsets = itemContentInsets
}
let itemAlpha: CGFloat
if isItemExpanded {
itemAlpha = 1.0
} else if component.expandedVideoState != nil && itemLayout.layout.videoColumn != nil {
itemAlpha = 0.0
} else {
itemAlpha = 1.0
}
let _ = itemView.view.update(
@ -752,14 +851,16 @@ final class VideoChatParticipantsComponent: Component {
isPresentation: videoParticipant.isPresentation,
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
isExpanded: isItemExpanded,
bottomInset: itemBottomInset,
isUIHidden: isItemUIHidden,
contentInsets: itemContentInsets,
controlInsets: itemControlInsets,
rootVideoLoadingEffectView: self.rootVideoLoadingEffectView,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if component.expandedVideoState?.mainParticipant == videoParticipantKey {
component.updateMainParticipant(nil)
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden)
} else {
component.updateMainParticipant(videoParticipantKey)
}
@ -770,6 +871,8 @@ final class VideoChatParticipantsComponent: Component {
)
if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil {
itemComponentView.layer.allowsGroupOpacity = true
if isItemExpanded {
if let expandedThumbnailsView = self.expandedThumbnailsView?.view {
self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView)
@ -781,12 +884,13 @@ final class VideoChatParticipantsComponent: Component {
}
itemComponentView.frame = itemFrame
itemComponentView.alpha = itemAlpha
if !resultingItemTransition.animation.isImmediate {
resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
}
if !resultingItemTransition.animation.isImmediate {
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
if !resultingItemTransition.animation.isImmediate && itemAlpha != 0.0 {
itemComponentView.layer.animateAlpha(from: 0.0, to: itemAlpha, duration: 0.1)
}
} else if isItemExpanded && itemComponentView.superview != self.expandedGridItemContainer {
let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer)
@ -820,6 +924,14 @@ final class VideoChatParticipantsComponent: Component {
if !itemView.isCollapsing {
resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center)
resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
let resultingItemAlphaTransition: ComponentTransition
if !resultingItemTransition.animation.isImmediate {
resultingItemAlphaTransition = alphaTransition
} else {
resultingItemAlphaTransition = .immediate
}
resultingItemAlphaTransition.setAlpha(view: itemComponentView, alpha: itemAlpha)
}
}
}
@ -1017,6 +1129,18 @@ final class VideoChatParticipantsComponent: Component {
))
}*/
let expandedControlsAlpha: CGFloat = expandedVideoState.isUIHidden ? 0.0 : 1.0
let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha
/*if itemLayout.layout.videoColumn == nil {
if expandedVideoState.isUIHidden {
expandedThumbnailsAlpha = 0.0
} else {
expandedThumbnailsAlpha = 1.0
}
} else {
expandedThumbnailsAlpha = 0.0
}*/
var expandedThumbnailsTransition = transition
let expandedThumbnailsView: ComponentView<Empty>
if let current = self.expandedThumbnailsView {
@ -1046,16 +1170,11 @@ final class VideoChatParticipantsComponent: Component {
environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
)
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
switch itemLayout.layoutType {
case .vertical:
break
case .horizontal:
expandedThumbnailsFrame.origin.y -= itemLayout.expandedGrid.containerInsets.bottom
}
let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
if let expandedThumbnailsComponentView = expandedThumbnailsView.view {
if expandedThumbnailsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView)
expandedThumbnailsComponentView.alpha = expandedThumbnailsAlpha
let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) {
@ -1066,11 +1185,12 @@ final class VideoChatParticipantsComponent: Component {
expandedThumbnailsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.maxY - expandedThumbnailsSize.height), size: expandedThumbnailsFrame.size)
if !transition.animation.isImmediate {
if !transition.animation.isImmediate && expandedThumbnailsAlpha != 0.0 {
expandedThumbnailsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
transition.setFrame(view: expandedThumbnailsComponentView, frame: expandedThumbnailsFrame)
alphaTransition.setAlpha(view: expandedThumbnailsComponentView, alpha: expandedThumbnailsAlpha)
}
var expandedControlsTransition = transition
@ -1113,6 +1233,8 @@ final class VideoChatParticipantsComponent: Component {
if expandedControlsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedControlsComponentView)
expandedControlsComponentView.alpha = expandedControlsAlpha
let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) {
fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer)
@ -1122,11 +1244,12 @@ final class VideoChatParticipantsComponent: Component {
expandedControlsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.minY - previousExpandedGridItemContainerFrame.minY), size: expandedControlsFrame.size)
if !transition.animation.isImmediate {
if !transition.animation.isImmediate && expandedControlsAlpha != 0.0 {
expandedControlsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
transition.setFrame(view: expandedControlsComponentView, frame: expandedControlsFrame)
alphaTransition.setAlpha(view: expandedControlsComponentView, alpha: expandedControlsAlpha)
}
} else {
if let expandedThumbnailsView = self.expandedThumbnailsView {
@ -1142,7 +1265,7 @@ final class VideoChatParticipantsComponent: Component {
let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.maxY - expandedThumbnailsComponentView.bounds.height), size: expandedThumbnailsComponentView.bounds.size)
transition.setFrame(view: expandedThumbnailsComponentView, frame: targetThumbnailsFrame)
}
expandedThumbnailsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in
expandedThumbnailsComponentView.layer.animateAlpha(from: expandedThumbnailsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in
expandedThumbnailsComponentView?.removeFromSuperview()
})
} else {
@ -1163,7 +1286,7 @@ final class VideoChatParticipantsComponent: Component {
let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.minY), size: expandedControlsComponentView.bounds.size)
transition.setFrame(view: expandedControlsComponentView, frame: targetThumbnailsFrame)
}
expandedControlsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in
expandedControlsComponentView.layer.animateAlpha(from: expandedControlsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in
expandedControlsComponentView?.removeFromSuperview()
})
} else {
@ -1272,10 +1395,10 @@ final class VideoChatParticipantsComponent: Component {
let itemLayout = ItemLayout(
containerSize: availableSize,
layoutType: component.layoutType,
sideInset: component.sideInset,
collapsedContainerInsets: component.collapsedContainerInsets,
expandedContainerInsets: component.expandedContainerInsets,
layout: component.layout,
isUIHidden: component.expandedVideoState?.isUIHidden ?? false,
expandedInsets: component.expandedInsets,
safeInsets: component.safeInsets,
gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count,
listItemHeight: measureListItemSize.height,
@ -1290,9 +1413,9 @@ final class VideoChatParticipantsComponent: Component {
cornerRadius: 10.0
)),
environment: {},
containerSize: CGSize(width: itemLayout.list.containerSize.width, height: itemLayout.list.contentHeight())
containerSize: CGSize(width: itemLayout.listFrame.width - itemLayout.layout.mainColumn.insets.left - itemLayout.layout.mainColumn.insets.right, height: itemLayout.list.contentHeight())
)
let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackgroundSize)
let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: itemLayout.layout.mainColumn.insets.left, y: 0.0), size: listItemsBackgroundSize)
if let listItemsBackgroundView = self.listItemsBackground.view {
if listItemsBackgroundView.superview == nil {
self.listItemViewContainer.addSubview(listItemsBackgroundView)
@ -1375,12 +1498,11 @@ final class VideoChatParticipantsComponent: Component {
self.ignoreScrolling = false
switch component.layoutType {
case .vertical:
if itemLayout.layout.videoColumn == nil {
if self.gridItemViewContainer.superview !== self.scrollView {
self.scrollView.addSubview(self.gridItemViewContainer)
}
case .horizontal:
} else {
if self.gridItemViewContainer.superview !== self.separateVideoScrollView {
self.separateVideoScrollView.addSubview(self.gridItemViewContainer)
}

View File

@ -1041,9 +1041,9 @@ private final class VideoChatScreenComponent: Component {
}) {
if participant.peer.id != expandedParticipantsVideoState.mainParticipant.id {
if participant.presentationDescription != nil {
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden)
} else {
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden)
}
}
}
@ -1073,9 +1073,9 @@ private final class VideoChatScreenComponent: Component {
return false
}) {
if participant.presentationDescription != nil {
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden)
} else {
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden)
}
} else {
self.expandedParticipantsVideoState = nil
@ -1227,7 +1227,7 @@ private final class VideoChatScreenComponent: Component {
}
})
let sideInset: CGFloat = environment.safeInsets.left + 14.0
let sideInset: CGFloat = max(environment.safeInsets.left, 14.0)
let topInset: CGFloat = environment.statusBarHeight + 2.0
let navigationBarHeight: CGFloat = 61.0
@ -1333,35 +1333,60 @@ private final class VideoChatScreenComponent: Component {
)
}
let participantsLayoutType: VideoChatParticipantsComponent.LayoutType
if availableSize.width > 620.0 {
let maxSingleColumnWidth: CGFloat = 620.0
let isTwoColumnLayout: Bool
if availableSize.width > maxSingleColumnWidth {
if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) {
participantsLayoutType = .horizontal(VideoChatParticipantsComponent.LayoutType.Horizontal(
rightColumnWidth: 300.0,
columnSpacing: 8.0,
isCentered: false
))
isTwoColumnLayout = true
} else {
participantsLayoutType = .horizontal(VideoChatParticipantsComponent.LayoutType.Horizontal(
rightColumnWidth: 380.0,
columnSpacing: 0.0,
isCentered: true
))
isTwoColumnLayout = false
}
} else {
participantsLayoutType = .vertical
isTwoColumnLayout = false
}
let areButtonsCollapsed: Bool
let mainColumnWidth: CGFloat
let mainColumnSideInset: CGFloat
if isTwoColumnLayout {
areButtonsCollapsed = false
mainColumnWidth = 320.0
mainColumnSideInset = 0.0
} else {
areButtonsCollapsed = self.expandedParticipantsVideoState != nil
if availableSize.width > maxSingleColumnWidth {
mainColumnWidth = 420.0
mainColumnSideInset = 0.0
} else {
mainColumnWidth = availableSize.width
mainColumnSideInset = sideInset
}
}
let actionButtonDiameter: CGFloat = 56.0
let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter
let collapsedMicrophoneButtonDiameter: CGFloat = 116.0
var collapsedMicrophoneButtonDiameter: CGFloat = 116.0
let maxActionMicrophoneButtonSpacing: CGFloat = 38.0
let minActionMicrophoneButtonSpacing: CGFloat = 20.0
if actionButtonDiameter * 2.0 + collapsedMicrophoneButtonDiameter + maxActionMicrophoneButtonSpacing * 2.0 > mainColumnWidth {
collapsedMicrophoneButtonDiameter = mainColumnWidth - (actionButtonDiameter * 2.0 + minActionMicrophoneButtonSpacing * 2.0)
collapsedMicrophoneButtonDiameter = max(actionButtonDiameter, collapsedMicrophoneButtonDiameter)
}
let microphoneButtonDiameter: CGFloat
if case .horizontal = participantsLayoutType {
microphoneButtonDiameter = expandedMicrophoneButtonDiameter
if isTwoColumnLayout {
microphoneButtonDiameter = collapsedMicrophoneButtonDiameter
} else {
microphoneButtonDiameter = self.expandedParticipantsVideoState == nil ? collapsedMicrophoneButtonDiameter : expandedMicrophoneButtonDiameter
if areButtonsCollapsed {
microphoneButtonDiameter = expandedMicrophoneButtonDiameter
} else {
microphoneButtonDiameter = self.expandedParticipantsVideoState == nil ? collapsedMicrophoneButtonDiameter : expandedMicrophoneButtonDiameter
}
}
let buttonsSideInset: CGFloat = 42.0
@ -1370,36 +1395,96 @@ private final class VideoChatScreenComponent: Component {
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
let collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter))
let expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter))
let microphoneButtonFrame: CGRect
if case .horizontal = participantsLayoutType {
microphoneButtonFrame = expandedMicrophoneButtonFrame
} else {
if self.expandedParticipantsVideoState == nil {
microphoneButtonFrame = collapsedMicrophoneButtonFrame
var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter))
var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter))
if isTwoColumnLayout {
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden {
collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + sideInset + mainColumnWidth
} else {
microphoneButtonFrame = expandedMicrophoneButtonFrame
collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5)
}
expandedMicrophoneButtonFrame = collapsedMicrophoneButtonFrame
} else {
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden {
expandedMicrophoneButtonFrame.origin.y = availableSize.height + expandedMicrophoneButtonDiameter + 12.0
}
}
let collapsedParticipantsClippingY: CGFloat = collapsedMicrophoneButtonFrame.minY - 16.0
let expandedParticipantsClippingY: CGFloat = expandedMicrophoneButtonFrame.minY - 24.0
let microphoneButtonFrame: CGRect
if areButtonsCollapsed {
microphoneButtonFrame = expandedMicrophoneButtonFrame
} else {
microphoneButtonFrame = collapsedMicrophoneButtonFrame
}
let collapsedParticipantsClippingY: CGFloat
collapsedParticipantsClippingY = collapsedMicrophoneButtonFrame.minY - 16.0
let expandedParticipantsClippingY: CGFloat
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden {
if isTwoColumnLayout {
expandedParticipantsClippingY = expandedMicrophoneButtonFrame.minY - 24.0
} else {
expandedParticipantsClippingY = availableSize.height - max(14.0, environment.safeInsets.bottom)
}
} else {
expandedParticipantsClippingY = expandedMicrophoneButtonFrame.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 = availableSize
let participantsCollapsedInsets: UIEdgeInsets
let participantsExpandedInsets: UIEdgeInsets
if case .horizontal = participantsLayoutType {
participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - (expandedMicrophoneButtonFrame.minY - 16.0), right: environment.safeInsets.right)
participantsExpandedInsets = participantsCollapsedInsets
let columnSpacing: CGFloat = 14.0
let participantsLayout: VideoChatParticipantsComponent.Layout
if isTwoColumnLayout {
let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset)
let videoColumnWidth: CGFloat = max(10.0, availableSize.width - sideInset * 2.0 - mainColumnWidth - columnSpacing)
participantsLayout = VideoChatParticipantsComponent.Layout(
videoColumn: VideoChatParticipantsComponent.Layout.Column(
width: videoColumnWidth,
insets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: max(14.0, environment.safeInsets.bottom), right: 0.0)
),
mainColumn: VideoChatParticipantsComponent.Layout.Column(
width: mainColumnWidth,
insets: mainColumnInsets
),
columnSpacing: columnSpacing
)
} else {
participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - collapsedParticipantsClippingY, right: environment.safeInsets.right)
participantsExpandedInsets = UIEdgeInsets(top: environment.statusBarHeight, left: environment.safeInsets.left, bottom: availableSize.height - expandedParticipantsClippingY, right: environment.safeInsets.right)
let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset)
participantsLayout = VideoChatParticipantsComponent.Layout(
videoColumn: nil,
mainColumn: VideoChatParticipantsComponent.Layout.Column(
width: mainColumnWidth,
insets: mainColumnInsets
),
columnSpacing: columnSpacing
)
}
let participantsSafeInsets = UIEdgeInsets(
top: environment.statusBarHeight,
left: environment.safeInsets.left,
bottom: max(14.0, environment.safeInsets.bottom),
right: environment.safeInsets.right
)
let participantsExpandedInsets: UIEdgeInsets
if isTwoColumnLayout {
participantsExpandedInsets = UIEdgeInsets(
top: navigationHeight,
left: max(14.0, participantsSafeInsets.left),
bottom: participantsSafeInsets.bottom,
right: max(14.0, participantsSafeInsets.right)
)
} else {
participantsExpandedInsets = UIEdgeInsets(
top: participantsSafeInsets.top,
left: participantsSafeInsets.left,
bottom: availableSize.height - expandedParticipantsClippingY,
right: participantsSafeInsets.right
)
}
let _ = self.participants.update(
@ -1411,10 +1496,9 @@ private final class VideoChatScreenComponent: Component {
expandedVideoState: self.expandedParticipantsVideoState,
theme: environment.theme,
strings: environment.strings,
layoutType: participantsLayoutType,
collapsedContainerInsets: participantsCollapsedInsets,
expandedContainerInsets: participantsExpandedInsets,
sideInset: sideInset,
layout: participantsLayout,
expandedInsets: participantsExpandedInsets,
safeInsets: participantsSafeInsets,
updateMainParticipant: { [weak self] key in
guard let self else {
return
@ -1423,7 +1507,7 @@ private final class VideoChatScreenComponent: Component {
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key {
return
}
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false)
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: self.expandedParticipantsVideoState?.isUIHidden ?? false)
self.state?.updated(transition: .spring(duration: 0.4))
} else if self.expandedParticipantsVideoState != nil {
self.expandedParticipantsVideoState = nil
@ -1439,7 +1523,25 @@ private final class VideoChatScreenComponent: Component {
}
let updatedExpandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(
mainParticipant: expandedParticipantsVideoState.mainParticipant,
isMainParticipantPinned: isPinned
isMainParticipantPinned: isPinned,
isUIHidden: expandedParticipantsVideoState.isUIHidden
)
if self.expandedParticipantsVideoState != updatedExpandedParticipantsVideoState {
self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState
self.state?.updated(transition: .spring(duration: 0.4))
}
},
updateIsExpandedUIHidden: { [weak self] isUIHidden in
guard let self else {
return
}
guard let expandedParticipantsVideoState = self.expandedParticipantsVideoState else {
return
}
let updatedExpandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(
mainParticipant: expandedParticipantsVideoState.mainParticipant,
isMainParticipantPinned: expandedParticipantsVideoState.isMainParticipantPinned,
isUIHidden: isUIHidden
)
if self.expandedParticipantsVideoState != updatedExpandedParticipantsVideoState {
self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState
@ -1484,13 +1586,6 @@ private final class VideoChatScreenComponent: Component {
actionButtonMicrophoneState = .connecting
}
let areButtonsCollapsed: Bool
if case .horizontal = participantsLayoutType {
areButtonsCollapsed = true
} else {
areButtonsCollapsed = self.expandedParticipantsVideoState != nil
}
let _ = self.microphoneButton.update(
transition: transition,
component: AnyComponent(VideoChatMicButtonComponent(