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

View File

@ -6,50 +6,49 @@ import MultilineTextComponent
import TelegramPresentationData import TelegramPresentationData
import AppBundle import AppBundle
import LottieComponent import LottieComponent
import BundleIconComponent
final class VideoChatMuteIconComponent: Component { final class VideoChatMuteIconComponent: Component {
enum Content: Equatable {
case mute(isFilled: Bool, isMuted: Bool)
case screenshare
}
let color: UIColor let color: UIColor
let isFilled: Bool let content: Content
let isMuted: Bool
init( init(
color: UIColor, color: UIColor,
isFilled: Bool, content: Content
isMuted: Bool
) { ) {
self.color = color self.color = color
self.isFilled = isFilled self.content = content
self.isMuted = isMuted
} }
static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool { static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool {
if lhs.color != rhs.color { if lhs.color != rhs.color {
return false return false
} }
if lhs.isFilled != rhs.isFilled { if lhs.content != rhs.content {
return false
}
if lhs.isMuted != rhs.isMuted {
return false return false
} }
return true return true
} }
final class View: HighlightTrackingButton { final class View: HighlightTrackingButton {
private let icon: VoiceChatMicrophoneNode private var icon: VoiceChatMicrophoneNode?
private var scheenshareIcon: ComponentView<Empty>?
private var component: VideoChatMuteIconComponent? private var component: VideoChatMuteIconComponent?
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var contentImage: UIImage? private var contentImage: UIImage?
var iconView: UIView { var iconView: UIView? {
return self.icon.view return self.icon?.view
} }
override init(frame: CGRect) { override init(frame: CGRect) {
self.icon = VoiceChatMicrophoneNode()
super.init(frame: frame) super.init(frame: frame)
} }
@ -65,14 +64,59 @@ final class VideoChatMuteIconComponent: Component {
self.component = component self.component = component
let animationSize = availableSize if case let .mute(isFilled, isMuted) = component.content {
let icon: VoiceChatMicrophoneNode
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) if let current = self.icon {
if self.icon.view.superview == nil { icon = current
self.addSubview(self.icon.view) } 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 return availableSize
} }

View File

@ -61,8 +61,7 @@ final class VideoChatParticipantStatusComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatMuteIconComponent( component: AnyComponent(VideoChatMuteIconComponent(
color: .white, color: .white,
isFilled: false, content: .mute(isFilled: false, isMuted: component.isMuted && !component.isSpeaking)
isMuted: component.isMuted && !component.isSpeaking
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 36.0, height: 36.0) containerSize: CGSize(width: 36.0, height: 36.0)
@ -80,7 +79,9 @@ final class VideoChatParticipantStatusComponent: Component {
} else { } else {
tintTransition = .immediate 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 return size

View File

@ -40,7 +40,9 @@ final class VideoChatParticipantVideoComponent: Component {
let isPresentation: Bool let isPresentation: Bool
let isSpeaking: Bool let isSpeaking: Bool
let isExpanded: Bool let isExpanded: Bool
let bottomInset: CGFloat let isUIHidden: Bool
let contentInsets: UIEdgeInsets
let controlInsets: UIEdgeInsets
weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
let action: (() -> Void)? let action: (() -> Void)?
@ -50,7 +52,9 @@ final class VideoChatParticipantVideoComponent: Component {
isPresentation: Bool, isPresentation: Bool,
isSpeaking: Bool, isSpeaking: Bool,
isExpanded: Bool, isExpanded: Bool,
bottomInset: CGFloat, isUIHidden: Bool,
contentInsets: UIEdgeInsets,
controlInsets: UIEdgeInsets,
rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?, rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?,
action: (() -> Void)? action: (() -> Void)?
) { ) {
@ -59,7 +63,9 @@ final class VideoChatParticipantVideoComponent: Component {
self.isPresentation = isPresentation self.isPresentation = isPresentation
self.isSpeaking = isSpeaking self.isSpeaking = isSpeaking
self.isExpanded = isExpanded self.isExpanded = isExpanded
self.bottomInset = bottomInset self.isUIHidden = isUIHidden
self.contentInsets = contentInsets
self.controlInsets = controlInsets
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
self.action = action self.action = action
} }
@ -77,7 +83,13 @@ final class VideoChatParticipantVideoComponent: Component {
if lhs.isExpanded != rhs.isExpanded { if lhs.isExpanded != rhs.isExpanded {
return false 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 return false
} }
if (lhs.action == nil) != (rhs.action == nil) { if (lhs.action == nil) != (rhs.action == nil) {
@ -153,6 +165,15 @@ final class VideoChatParticipantVideoComponent: Component {
self.component = component self.component = component
self.componentState = state 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 nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true) let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4) self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4)
@ -210,25 +231,26 @@ final class VideoChatParticipantVideoComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatMuteIconComponent( component: AnyComponent(VideoChatMuteIconComponent(
color: .white, color: .white,
isFilled: true, content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking)
isMuted: component.participant.muteState != nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 36.0, height: 36.0) containerSize: CGSize(width: 36.0, height: 36.0)
) )
let muteStatusFrame: CGRect let muteStatusFrame: CGRect
if component.isExpanded { 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 { } 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 let muteStatusView = self.muteStatus.view {
if muteStatusView.superview == nil { if muteStatusView.superview == nil {
self.addSubview(muteStatusView) self.addSubview(muteStatusView)
muteStatusView.alpha = controlsAlpha
} }
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7) transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7)
alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha)
} }
let titleSize = self.title.update( let titleSize = self.title.update(
@ -241,18 +263,20 @@ final class VideoChatParticipantVideoComponent: Component {
) )
let titleFrame: CGRect let titleFrame: CGRect
if component.isExpanded { 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 { } 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 let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView) self.addSubview(titleView)
titleView.alpha = controlsAlpha
} }
transition.setPosition(view: titleView, position: titleFrame.origin) transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825) 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 { 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 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 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 rotatedVideoResolution = videoResolution
var rotatedVideoFrame = videoFrame var rotatedVideoFrame = videoFrame
@ -408,6 +432,7 @@ final class VideoChatParticipantVideoComponent: Component {
rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center) rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center)
rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.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.setPosition(layer: videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))

View File

@ -12,23 +12,28 @@ import TelegramPresentationData
import PeerListItemComponent import PeerListItemComponent
final class VideoChatParticipantsComponent: Component { final class VideoChatParticipantsComponent: Component {
enum LayoutType: Equatable { struct Layout: Equatable {
struct Horizontal: Equatable { struct Column: Equatable {
var rightColumnWidth: CGFloat var width: CGFloat
var insets: UIEdgeInsets
init(width: CGFloat, insets: UIEdgeInsets) {
self.width = width
self.insets = insets
}
}
var videoColumn: Column?
var mainColumn: Column
var columnSpacing: CGFloat var columnSpacing: CGFloat
var isCentered: Bool
init(rightColumnWidth: CGFloat, columnSpacing: CGFloat, isCentered: Bool) { init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat) {
self.rightColumnWidth = rightColumnWidth self.videoColumn = videoColumn
self.mainColumn = mainColumn
self.columnSpacing = columnSpacing self.columnSpacing = columnSpacing
self.isCentered = isCentered
} }
} }
case vertical
case horizontal(Horizontal)
}
final class Participants: Equatable { final class Participants: Equatable {
let myPeerId: EnginePeer.Id let myPeerId: EnginePeer.Id
let participants: [GroupCallParticipantsContext.Participant] let participants: [GroupCallParticipantsContext.Participant]
@ -75,10 +80,12 @@ final class VideoChatParticipantsComponent: Component {
final class ExpandedVideoState: Equatable { final class ExpandedVideoState: Equatable {
let mainParticipant: VideoParticipantKey let mainParticipant: VideoParticipantKey
let isMainParticipantPinned: Bool let isMainParticipantPinned: Bool
let isUIHidden: Bool
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool) { init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool, isUIHidden: Bool) {
self.mainParticipant = mainParticipant self.mainParticipant = mainParticipant
self.isMainParticipantPinned = isMainParticipantPinned self.isMainParticipantPinned = isMainParticipantPinned
self.isUIHidden = isUIHidden
} }
static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool { static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool {
@ -91,6 +98,9 @@ final class VideoChatParticipantsComponent: Component {
if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned { if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned {
return false return false
} }
if lhs.isUIHidden != rhs.isUIHidden {
return false
}
return true return true
} }
} }
@ -101,12 +111,12 @@ final class VideoChatParticipantsComponent: Component {
let expandedVideoState: ExpandedVideoState? let expandedVideoState: ExpandedVideoState?
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let layoutType: LayoutType let layout: Layout
let collapsedContainerInsets: UIEdgeInsets let expandedInsets: UIEdgeInsets
let expandedContainerInsets: UIEdgeInsets let safeInsets: UIEdgeInsets
let sideInset: CGFloat
let updateMainParticipant: (VideoParticipantKey?) -> Void let updateMainParticipant: (VideoParticipantKey?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void
init( init(
call: PresentationGroupCall, call: PresentationGroupCall,
@ -115,12 +125,12 @@ final class VideoChatParticipantsComponent: Component {
expandedVideoState: ExpandedVideoState?, expandedVideoState: ExpandedVideoState?,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
layoutType: LayoutType, layout: Layout,
collapsedContainerInsets: UIEdgeInsets, expandedInsets: UIEdgeInsets,
expandedContainerInsets: UIEdgeInsets, safeInsets: UIEdgeInsets,
sideInset: CGFloat,
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void, updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void
) { ) {
self.call = call self.call = call
self.participants = participants self.participants = participants
@ -128,12 +138,12 @@ final class VideoChatParticipantsComponent: Component {
self.expandedVideoState = expandedVideoState self.expandedVideoState = expandedVideoState
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.layoutType = layoutType self.layout = layout
self.collapsedContainerInsets = collapsedContainerInsets self.expandedInsets = expandedInsets
self.expandedContainerInsets = expandedContainerInsets self.safeInsets = safeInsets
self.sideInset = sideInset
self.updateMainParticipant = updateMainParticipant self.updateMainParticipant = updateMainParticipant
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
} }
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
@ -152,16 +162,13 @@ final class VideoChatParticipantsComponent: Component {
if lhs.strings !== rhs.strings { if lhs.strings !== rhs.strings {
return false return false
} }
if lhs.layoutType != rhs.layoutType { if lhs.layout != rhs.layout {
return false return false
} }
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets { if lhs.expandedInsets != rhs.expandedInsets {
return false return false
} }
if lhs.expandedContainerInsets != rhs.expandedContainerInsets { if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.sideInset != rhs.sideInset {
return false return false
} }
return true return true
@ -178,40 +185,84 @@ final class VideoChatParticipantsComponent: Component {
let containerSize: CGSize let containerSize: CGSize
let sideInset: CGFloat let sideInset: CGFloat
let itemCount: Int let itemCount: Int
let isDedicatedColumn: Bool
let itemSize: CGSize let itemSize: CGSize
let itemSpacing: CGFloat let itemSpacing: CGFloat
let lastItemSize: CGFloat let lastItemSize: CGFloat
let lastRowItemCount: Int
let lastRowItemSize: CGFloat
let itemsPerRow: Int 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.containerSize = containerSize
self.sideInset = sideInset self.sideInset = sideInset
self.itemCount = itemCount self.itemCount = itemCount
self.isDedicatedColumn = isDedicatedColumn
let width: CGFloat = containerSize.width - sideInset * 2.0 let width: CGFloat = containerSize.width - sideInset * 2.0
self.itemSpacing = 4.0 self.itemSpacing = 4.0
let itemsPerRow: Int let itemsPerRow: Int
if isDedicatedColumn {
if itemCount <= 2 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
} else {
if itemCount == 1 { if itemCount == 1 {
itemsPerRow = 1 itemsPerRow = 1
} else { } else {
itemsPerRow = 2 itemsPerRow = 2
} }
}
self.itemsPerRow = Int(itemsPerRow) self.itemsPerRow = Int(itemsPerRow)
let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow)) let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow))
let itemHeight = min(180.0, itemWidth) 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) 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 { func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow let row = index / self.itemsPerRow
let column = 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 return frame
} }
@ -237,21 +288,37 @@ final class VideoChatParticipantsComponent: Component {
struct ExpandedGrid { struct ExpandedGrid {
let containerSize: CGSize let containerSize: CGSize
let layoutType: LayoutType let layout: Layout
let containerInsets: UIEdgeInsets 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.containerSize = containerSize
self.layoutType = layoutType self.layout = layout
self.containerInsets = containerInsets self.expandedInsets = expandedInsets
self.isUIHidden = isUIHidden
} }
func itemContainerFrame() -> CGRect { func itemContainerFrame() -> CGRect {
switch self.layoutType { let containerInsets: UIEdgeInsets
case .vertical: if self.isUIHidden {
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)) containerInsets = UIEdgeInsets()
case .horizontal: } else {
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 = 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 containerSize: CGSize
let layoutType: LayoutType let layout: Layout
let collapsedContainerInsets: UIEdgeInsets let isUIHidden: Bool
let sideInset: CGFloat let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let grid: Grid let grid: Grid
let expandedGrid: ExpandedGrid let expandedGrid: ExpandedGrid
let list: List let list: List
@ -320,32 +388,41 @@ final class VideoChatParticipantsComponent: Component {
let scrollClippingFrame: CGRect let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: 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.containerSize = containerSize
self.layoutType = layoutType self.layout = layout
self.collapsedContainerInsets = collapsedContainerInsets self.isUIHidden = isUIHidden
self.sideInset = sideInset self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
let listWidth: CGFloat = layout.mainColumn.width
let gridWidth: CGFloat let gridWidth: CGFloat
let listWidth: CGFloat let gridSideInset: CGFloat
switch layoutType { let gridContainerHeight: CGFloat
case .vertical: if let videoColumn = layout.videoColumn {
listWidth = containerSize.width - sideInset * 2.0 gridWidth = videoColumn.width
gridSideInset = videoColumn.insets.left
gridContainerHeight = containerSize.height - videoColumn.insets.top - videoColumn.insets.bottom
} else {
gridWidth = listWidth gridWidth = listWidth
case let .horizontal(horizontal): gridSideInset = layout.mainColumn.insets.left
listWidth = horizontal.rightColumnWidth gridContainerHeight = containerSize.height
gridWidth = max(10.0, containerSize.width - sideInset * 2.0 - horizontal.rightColumnWidth - horizontal.columnSpacing)
} }
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount) self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layoutType: layoutType, containerInsets: expandedContainerInsets) self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.spacing = 4.0 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 var listOffsetY: CGFloat = self.gridOffsetY
if case .vertical = layoutType { if layout.videoColumn == nil {
if self.grid.itemCount != 0 { if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight() listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing listOffsetY += self.spacing
@ -353,55 +430,54 @@ final class VideoChatParticipantsComponent: Component {
} }
self.listOffsetY = listOffsetY self.listOffsetY = listOffsetY
switch layoutType { if let videoColumn = layout.videoColumn {
case .vertical: let columnsWidth: CGFloat = videoColumn.width + layout.columnSpacing + layout.mainColumn.width
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)) let columnsSideInset: CGFloat = floorToScreenPixels((containerSize.width - columnsWidth) * 0.5)
self.listFrame = CGRect(origin: CGPoint(), size: containerSize)
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: -containerSize.width, y: 0.0), size: containerSize) var separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), 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))
case let .horizontal(horizontal): var listFrame = CGRect(origin: CGPoint(x: separateVideoGridFrame.maxX + layout.columnSpacing, y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
if horizontal.isCentered { if isUIHidden {
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - horizontal.rightColumnWidth) * 0.5), y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height)) listFrame.origin.x += columnsSideInset + layout.mainColumn.width
} else { separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height))
self.listFrame = CGRect(origin: CGPoint(x: containerSize.width - self.sideInset - horizontal.rightColumnWidth, y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, 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.separateVideoGridFrame = separateVideoGridFrame
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top)) 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 { func contentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY var result: CGFloat = self.gridOffsetY
switch self.layoutType { if self.layout.videoColumn == nil {
case .vertical:
if self.grid.itemCount != 0 { if self.grid.itemCount != 0 {
result += self.grid.contentHeight() result += self.grid.contentHeight()
result += self.spacing result += self.spacing
} }
case .horizontal:
break
} }
result += self.list.contentHeight() result += self.list.contentHeight()
result += self.collapsedContainerInsets.bottom result += self.layout.mainColumn.insets.bottom
result += 24.0 result += 24.0
return result return result
} }
func separateVideoGridContentHeight() -> CGFloat { func separateVideoGridContentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY var result: CGFloat = self.gridOffsetY
switch self.layoutType { if let videoColumn = self.layout.videoColumn {
case .vertical:
break
case .horizontal:
if self.grid.itemCount != 0 { if self.grid.itemCount != 0 {
result += self.grid.contentHeight() result += self.grid.contentHeight()
} }
result += videoColumn.insets.bottom
} }
result += self.collapsedContainerInsets.bottom
result += 24.0
return result return result
} }
@ -414,11 +490,10 @@ final class VideoChatParticipantsComponent: Component {
} }
func gridItemContainerFrame() -> CGRect { func gridItemContainerFrame() -> CGRect {
switch self.layoutType { if let _ = self.layout.videoColumn {
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:
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight())) 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 { func listItemContainerFrame() -> CGRect {
switch self.layoutType { if let _ = self.layout.videoColumn {
case .vertical: return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.list.contentHeight()))
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight())) } else {
case .horizontal: return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.containerSize.width, height: self.list.contentHeight()))
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.listFrame.width, height: self.list.contentHeight()))
} }
} }
@ -620,6 +694,13 @@ final class VideoChatParticipantsComponent: Component {
return return
} }
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.2)
} else {
alphaTransition = .immediate
}
let gridWasEmpty = self.appliedGridIsEmpty let gridWasEmpty = self.appliedGridIsEmpty
let gridIsEmpty = self.gridParticipants.isEmpty let gridIsEmpty = self.gridParticipants.isEmpty
self.appliedGridIsEmpty = gridIsEmpty self.appliedGridIsEmpty = gridIsEmpty
@ -637,27 +718,26 @@ final class VideoChatParticipantsComponent: Component {
if component.expandedVideoState != nil { if component.expandedVideoState != nil {
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame() expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
} else { } else {
switch itemLayout.layoutType { if let videoColumn = itemLayout.layout.videoColumn {
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:
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: itemLayout.separateVideoScrollClippingFrame.minX, dy: 0.0).offsetBy(dx: 0.0, dy: -self.separateVideoScrollView.bounds.minY) 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 { if expandedGridItemContainerFrame.origin.y < videoColumn.insets.top {
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y expandedGridItemContainerFrame.size.height -= videoColumn.insets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top expandedGridItemContainerFrame.origin.y = videoColumn.insets.top
} }
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height { if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height {
expandedGridItemContainerFrame.size.height -= (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 { if expandedGridItemContainerFrame.size.height < 0.0 {
expandedGridItemContainerFrame.size.height = 0.0 expandedGridItemContainerFrame.size.height = 0.0
@ -670,10 +750,9 @@ final class VideoChatParticipantsComponent: Component {
var validGridItemIndices: [Int] = [] var validGridItemIndices: [Int] = []
let visibleGridItemRange: (minIndex: Int, maxIndex: Int) let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
switch itemLayout.layoutType { if itemLayout.layout.videoColumn == nil {
case .vertical:
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds) visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
case .horizontal: } else {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds) visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
} }
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex { if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
@ -707,9 +786,15 @@ final class VideoChatParticipantsComponent: Component {
} }
var isItemExpanded = false var isItemExpanded = false
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { var isItemUIHidden = false
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant == videoParticipantKey {
isItemExpanded = true isItemExpanded = true
} }
if expandedVideoState.isUIHidden {
isItemUIHidden = true
}
}
var suppressItemExpansionCollapseAnimation = false var suppressItemExpansionCollapseAnimation = false
if isItemExpanded { if isItemExpanded {
@ -734,14 +819,28 @@ final class VideoChatParticipantsComponent: Component {
itemFrame = itemLayout.gridItemFrame(at: index) itemFrame = itemLayout.gridItemFrame(at: index)
} }
var itemBottomInset: CGFloat = isItemExpanded ? 96.0 : 0.0 let itemContentInsets: UIEdgeInsets
switch itemLayout.layoutType {
case .vertical:
break
case .horizontal:
if isItemExpanded { if isItemExpanded {
itemBottomInset += itemLayout.expandedGrid.containerInsets.bottom 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( let _ = itemView.view.update(
@ -752,14 +851,16 @@ final class VideoChatParticipantsComponent: Component {
isPresentation: videoParticipant.isPresentation, isPresentation: videoParticipant.isPresentation,
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
isExpanded: isItemExpanded, isExpanded: isItemExpanded,
bottomInset: itemBottomInset, isUIHidden: isItemUIHidden,
contentInsets: itemContentInsets,
controlInsets: itemControlInsets,
rootVideoLoadingEffectView: self.rootVideoLoadingEffectView, rootVideoLoadingEffectView: self.rootVideoLoadingEffectView,
action: { [weak self] in action: { [weak self] in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
if component.expandedVideoState?.mainParticipant == videoParticipantKey { if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
component.updateMainParticipant(nil) component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden)
} else { } else {
component.updateMainParticipant(videoParticipantKey) component.updateMainParticipant(videoParticipantKey)
} }
@ -770,6 +871,8 @@ final class VideoChatParticipantsComponent: Component {
) )
if let itemComponentView = itemView.view.view { if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil { if itemComponentView.superview == nil {
itemComponentView.layer.allowsGroupOpacity = true
if isItemExpanded { if isItemExpanded {
if let expandedThumbnailsView = self.expandedThumbnailsView?.view { if let expandedThumbnailsView = self.expandedThumbnailsView?.view {
self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView) self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView)
@ -781,12 +884,13 @@ final class VideoChatParticipantsComponent: Component {
} }
itemComponentView.frame = itemFrame itemComponentView.frame = itemFrame
itemComponentView.alpha = itemAlpha
if !resultingItemTransition.animation.isImmediate { if !resultingItemTransition.animation.isImmediate {
resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0) resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
} }
if !resultingItemTransition.animation.isImmediate { if !resultingItemTransition.animation.isImmediate && itemAlpha != 0.0 {
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) itemComponentView.layer.animateAlpha(from: 0.0, to: itemAlpha, duration: 0.1)
} }
} else if isItemExpanded && itemComponentView.superview != self.expandedGridItemContainer { } else if isItemExpanded && itemComponentView.superview != self.expandedGridItemContainer {
let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer) let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer)
@ -820,6 +924,14 @@ final class VideoChatParticipantsComponent: Component {
if !itemView.isCollapsing { if !itemView.isCollapsing {
resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center) resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center)
resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) 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 var expandedThumbnailsTransition = transition
let expandedThumbnailsView: ComponentView<Empty> let expandedThumbnailsView: ComponentView<Empty>
if let current = self.expandedThumbnailsView { if let current = self.expandedThumbnailsView {
@ -1046,16 +1170,11 @@ final class VideoChatParticipantsComponent: Component {
environment: {}, environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size containerSize: itemLayout.expandedGrid.itemContainerFrame().size
) )
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) let 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
}
if let expandedThumbnailsComponentView = expandedThumbnailsView.view { if let expandedThumbnailsComponentView = expandedThumbnailsView.view {
if expandedThumbnailsComponentView.superview == nil { if expandedThumbnailsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView) self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView)
expandedThumbnailsComponentView.alpha = expandedThumbnailsAlpha
let fromReferenceFrame: CGRect let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { 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) 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) expandedThumbnailsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
transition.setFrame(view: expandedThumbnailsComponentView, frame: expandedThumbnailsFrame) transition.setFrame(view: expandedThumbnailsComponentView, frame: expandedThumbnailsFrame)
alphaTransition.setAlpha(view: expandedThumbnailsComponentView, alpha: expandedThumbnailsAlpha)
} }
var expandedControlsTransition = transition var expandedControlsTransition = transition
@ -1113,6 +1233,8 @@ final class VideoChatParticipantsComponent: Component {
if expandedControlsComponentView.superview == nil { if expandedControlsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedControlsComponentView) self.expandedGridItemContainer.addSubview(expandedControlsComponentView)
expandedControlsComponentView.alpha = expandedControlsAlpha
let fromReferenceFrame: CGRect let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { 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) 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) 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) expandedControlsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
transition.setFrame(view: expandedControlsComponentView, frame: expandedControlsFrame) transition.setFrame(view: expandedControlsComponentView, frame: expandedControlsFrame)
alphaTransition.setAlpha(view: expandedControlsComponentView, alpha: expandedControlsAlpha)
} }
} else { } else {
if let expandedThumbnailsView = self.expandedThumbnailsView { 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) let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.maxY - expandedThumbnailsComponentView.bounds.height), size: expandedThumbnailsComponentView.bounds.size)
transition.setFrame(view: expandedThumbnailsComponentView, frame: targetThumbnailsFrame) 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() expandedThumbnailsComponentView?.removeFromSuperview()
}) })
} else { } else {
@ -1163,7 +1286,7 @@ final class VideoChatParticipantsComponent: Component {
let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.minY), size: expandedControlsComponentView.bounds.size) let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.minY), size: expandedControlsComponentView.bounds.size)
transition.setFrame(view: expandedControlsComponentView, frame: targetThumbnailsFrame) 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() expandedControlsComponentView?.removeFromSuperview()
}) })
} else { } else {
@ -1272,10 +1395,10 @@ final class VideoChatParticipantsComponent: Component {
let itemLayout = ItemLayout( let itemLayout = ItemLayout(
containerSize: availableSize, containerSize: availableSize,
layoutType: component.layoutType, layout: component.layout,
sideInset: component.sideInset, isUIHidden: component.expandedVideoState?.isUIHidden ?? false,
collapsedContainerInsets: component.collapsedContainerInsets, expandedInsets: component.expandedInsets,
expandedContainerInsets: component.expandedContainerInsets, safeInsets: component.safeInsets,
gridItemCount: gridParticipants.count, gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count, listItemCount: listParticipants.count,
listItemHeight: measureListItemSize.height, listItemHeight: measureListItemSize.height,
@ -1290,9 +1413,9 @@ final class VideoChatParticipantsComponent: Component {
cornerRadius: 10.0 cornerRadius: 10.0
)), )),
environment: {}, 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 let listItemsBackgroundView = self.listItemsBackground.view {
if listItemsBackgroundView.superview == nil { if listItemsBackgroundView.superview == nil {
self.listItemViewContainer.addSubview(listItemsBackgroundView) self.listItemViewContainer.addSubview(listItemsBackgroundView)
@ -1375,12 +1498,11 @@ final class VideoChatParticipantsComponent: Component {
self.ignoreScrolling = false self.ignoreScrolling = false
switch component.layoutType { if itemLayout.layout.videoColumn == nil {
case .vertical:
if self.gridItemViewContainer.superview !== self.scrollView { if self.gridItemViewContainer.superview !== self.scrollView {
self.scrollView.addSubview(self.gridItemViewContainer) self.scrollView.addSubview(self.gridItemViewContainer)
} }
case .horizontal: } else {
if self.gridItemViewContainer.superview !== self.separateVideoScrollView { if self.gridItemViewContainer.superview !== self.separateVideoScrollView {
self.separateVideoScrollView.addSubview(self.gridItemViewContainer) 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.peer.id != expandedParticipantsVideoState.mainParticipant.id {
if participant.presentationDescription != nil { 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 { } 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 return false
}) { }) {
if participant.presentationDescription != nil { 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 { } 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 { } else {
self.expandedParticipantsVideoState = nil 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 topInset: CGFloat = environment.statusBarHeight + 2.0
let navigationBarHeight: CGFloat = 61.0 let navigationBarHeight: CGFloat = 61.0
@ -1333,36 +1333,61 @@ private final class VideoChatScreenComponent: Component {
) )
} }
let participantsLayoutType: VideoChatParticipantsComponent.LayoutType let maxSingleColumnWidth: CGFloat = 620.0
if availableSize.width > 620.0 { let isTwoColumnLayout: Bool
if availableSize.width > maxSingleColumnWidth {
if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) { if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) {
participantsLayoutType = .horizontal(VideoChatParticipantsComponent.LayoutType.Horizontal( isTwoColumnLayout = true
rightColumnWidth: 300.0,
columnSpacing: 8.0,
isCentered: false
))
} else { } else {
participantsLayoutType = .horizontal(VideoChatParticipantsComponent.LayoutType.Horizontal( isTwoColumnLayout = false
rightColumnWidth: 380.0,
columnSpacing: 0.0,
isCentered: true
))
} }
} else { } 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 actionButtonDiameter: CGFloat = 56.0
let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter
let collapsedMicrophoneButtonDiameter: CGFloat = 116.0 var collapsedMicrophoneButtonDiameter: CGFloat = 116.0
let maxActionMicrophoneButtonSpacing: CGFloat = 38.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 let microphoneButtonDiameter: CGFloat
if case .horizontal = participantsLayoutType { if isTwoColumnLayout {
microphoneButtonDiameter = collapsedMicrophoneButtonDiameter
} else {
if areButtonsCollapsed {
microphoneButtonDiameter = expandedMicrophoneButtonDiameter microphoneButtonDiameter = expandedMicrophoneButtonDiameter
} else { } else {
microphoneButtonDiameter = self.expandedParticipantsVideoState == nil ? collapsedMicrophoneButtonDiameter : expandedMicrophoneButtonDiameter microphoneButtonDiameter = self.expandedParticipantsVideoState == nil ? collapsedMicrophoneButtonDiameter : expandedMicrophoneButtonDiameter
} }
}
let buttonsSideInset: CGFloat = 42.0 let buttonsSideInset: CGFloat = 42.0
@ -1370,36 +1395,96 @@ private final class VideoChatScreenComponent: Component {
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) 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)) 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))
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)) 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 {
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 microphoneButtonFrame: CGRect let microphoneButtonFrame: CGRect
if case .horizontal = participantsLayoutType { if areButtonsCollapsed {
microphoneButtonFrame = expandedMicrophoneButtonFrame microphoneButtonFrame = expandedMicrophoneButtonFrame
} else { } else {
if self.expandedParticipantsVideoState == nil {
microphoneButtonFrame = collapsedMicrophoneButtonFrame microphoneButtonFrame = collapsedMicrophoneButtonFrame
} else {
microphoneButtonFrame = expandedMicrophoneButtonFrame
}
} }
let collapsedParticipantsClippingY: CGFloat = collapsedMicrophoneButtonFrame.minY - 16.0 let collapsedParticipantsClippingY: CGFloat
let expandedParticipantsClippingY: CGFloat = expandedMicrophoneButtonFrame.minY - 24.0 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 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 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 participantsSize = availableSize
let participantsCollapsedInsets: UIEdgeInsets
let participantsExpandedInsets: UIEdgeInsets
if case .horizontal = participantsLayoutType { let columnSpacing: CGFloat = 14.0
participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - (expandedMicrophoneButtonFrame.minY - 16.0), right: environment.safeInsets.right) let participantsLayout: VideoChatParticipantsComponent.Layout
participantsExpandedInsets = participantsCollapsedInsets 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 { } else {
participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - collapsedParticipantsClippingY, right: environment.safeInsets.right) let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset)
participantsExpandedInsets = UIEdgeInsets(top: environment.statusBarHeight, left: environment.safeInsets.left, bottom: availableSize.height - expandedParticipantsClippingY, right: environment.safeInsets.right) 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( let _ = self.participants.update(
@ -1411,10 +1496,9 @@ private final class VideoChatScreenComponent: Component {
expandedVideoState: self.expandedParticipantsVideoState, expandedVideoState: self.expandedParticipantsVideoState,
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
layoutType: participantsLayoutType, layout: participantsLayout,
collapsedContainerInsets: participantsCollapsedInsets, expandedInsets: participantsExpandedInsets,
expandedContainerInsets: participantsExpandedInsets, safeInsets: participantsSafeInsets,
sideInset: sideInset,
updateMainParticipant: { [weak self] key in updateMainParticipant: { [weak self] key in
guard let self else { guard let self else {
return return
@ -1423,7 +1507,7 @@ private final class VideoChatScreenComponent: Component {
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key { if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key {
return 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)) self.state?.updated(transition: .spring(duration: 0.4))
} else if self.expandedParticipantsVideoState != nil { } else if self.expandedParticipantsVideoState != nil {
self.expandedParticipantsVideoState = nil self.expandedParticipantsVideoState = nil
@ -1439,7 +1523,25 @@ private final class VideoChatScreenComponent: Component {
} }
let updatedExpandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState( let updatedExpandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(
mainParticipant: expandedParticipantsVideoState.mainParticipant, 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 { if self.expandedParticipantsVideoState != updatedExpandedParticipantsVideoState {
self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState
@ -1484,13 +1586,6 @@ private final class VideoChatScreenComponent: Component {
actionButtonMicrophoneState = .connecting actionButtonMicrophoneState = .connecting
} }
let areButtonsCollapsed: Bool
if case .horizontal = participantsLayoutType {
areButtonsCollapsed = true
} else {
areButtonsCollapsed = self.expandedParticipantsVideoState != nil
}
let _ = self.microphoneButton.update( let _ = self.microphoneButton.update(
transition: transition, transition: transition,
component: AnyComponent(VideoChatMicButtonComponent( component: AnyComponent(VideoChatMicButtonComponent(