Swiftgram/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift
2025-05-05 18:04:32 +02:00

2109 lines
112 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import AccountContext
import PlainButtonComponent
import SwiftSignalKit
import MultilineTextComponent
import TelegramPresentationData
import PeerListItemComponent
import ContextUI
import CallScreen
final class VideoChatParticipantsComponent: Component {
struct Layout: Equatable {
struct Column: Equatable {
var width: CGFloat
var insets: UIEdgeInsets
init(width: CGFloat, insets: UIEdgeInsets) {
self.width = width
self.insets = insets
}
}
var leftInset: CGFloat
var rightInset: CGFloat
var videoColumn: Column?
var mainColumn: Column
var columnSpacing: CGFloat
var isMainColumnHidden: Bool
init(leftInset: CGFloat, rightInset: CGFloat, videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat, isMainColumnHidden: Bool) {
self.leftInset = leftInset
self.rightInset = rightInset
self.videoColumn = videoColumn
self.mainColumn = mainColumn
self.columnSpacing = columnSpacing
self.isMainColumnHidden = isMainColumnHidden
}
}
final class Participants: Equatable {
enum InviteType: Equatable {
case invite(isMultipleUsers: Bool)
case shareLink
}
struct InviteOption: Equatable {
let id: Int
let type: InviteType
init(id: Int, type: InviteType) {
self.id = id
self.type = type
}
}
let myPeerId: EnginePeer.Id
let participants: [GroupCallParticipantsContext.Participant]
let totalCount: Int
let loadMoreToken: String?
let inviteOptions: [InviteOption]
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) {
self.myPeerId = myPeerId
self.participants = participants
self.totalCount = totalCount
self.loadMoreToken = loadMoreToken
self.inviteOptions = inviteOptions
}
static func ==(lhs: Participants, rhs: Participants) -> Bool {
if lhs === rhs {
return true
}
if lhs.myPeerId != rhs.myPeerId {
return false
}
if lhs.participants != rhs.participants {
return false
}
if lhs.totalCount != rhs.totalCount {
return false
}
if lhs.loadMoreToken != rhs.loadMoreToken {
return false
}
if lhs.inviteOptions != rhs.inviteOptions {
return false
}
return true
}
}
struct VideoParticipantKey: Hashable {
var id: GroupCallParticipantsContext.Participant.Id
var isPresentation: Bool
init(id: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool) {
self.id = id
self.isPresentation = isPresentation
}
}
final class ExpandedVideoState: Equatable {
let mainParticipant: VideoParticipantKey
let isMainParticipantPinned: Bool
let isUIHidden: Bool
init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool, isUIHidden: Bool) {
self.mainParticipant = mainParticipant
self.isMainParticipantPinned = isMainParticipantPinned
self.isUIHidden = isUIHidden
}
static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool {
if lhs === rhs {
return true
}
if lhs.mainParticipant != rhs.mainParticipant {
return false
}
if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned {
return false
}
if lhs.isUIHidden != rhs.isUIHidden {
return false
}
return true
}
}
final class EventCycleState {
var ignoreScrolling: Bool = false
init() {
}
}
let call: VideoChatCall
let participants: Participants?
let invitedPeers: [VideoChatScreenComponent.InvitedPeer]
let speakingParticipants: Set<EnginePeer.Id>
let expandedVideoState: ExpandedVideoState?
let maxVideoQuality: Int
let theme: PresentationTheme
let strings: PresentationStrings
let layout: Layout
let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let interfaceOrientation: UIInterfaceOrientation
let enableVideoSharpening: Bool
let openParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void
let openInvitedParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void
let openInviteMembers: (Participants.InviteType) -> Void
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
init(
call: VideoChatCall,
participants: Participants?,
invitedPeers: [VideoChatScreenComponent.InvitedPeer],
speakingParticipants: Set<EnginePeer.Id>,
expandedVideoState: ExpandedVideoState?,
maxVideoQuality: Int,
theme: PresentationTheme,
strings: PresentationStrings,
layout: Layout,
expandedInsets: UIEdgeInsets,
safeInsets: UIEdgeInsets,
interfaceOrientation: UIInterfaceOrientation,
enableVideoSharpening: Bool,
openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void,
openInvitedParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void,
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
openInviteMembers: @escaping (Participants.InviteType) -> Void,
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
) {
self.call = call
self.participants = participants
self.invitedPeers = invitedPeers
self.speakingParticipants = speakingParticipants
self.expandedVideoState = expandedVideoState
self.maxVideoQuality = maxVideoQuality
self.theme = theme
self.strings = strings
self.layout = layout
self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
self.interfaceOrientation = interfaceOrientation
self.enableVideoSharpening = enableVideoSharpening
self.openParticipantContextMenu = openParticipantContextMenu
self.openInvitedParticipantContextMenu = openInvitedParticipantContextMenu
self.updateMainParticipant = updateMainParticipant
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
self.openInviteMembers = openInviteMembers
self.visibleParticipantsUpdated = visibleParticipantsUpdated
}
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
if lhs.call != rhs.call {
return false
}
if lhs.participants != rhs.participants {
return false
}
if lhs.invitedPeers != rhs.invitedPeers {
return false
}
if lhs.speakingParticipants != rhs.speakingParticipants {
return false
}
if lhs.expandedVideoState != rhs.expandedVideoState {
return false
}
if lhs.maxVideoQuality != rhs.maxVideoQuality {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.layout != rhs.layout {
return false
}
if lhs.expandedInsets != rhs.expandedInsets {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if lhs.enableVideoSharpening != rhs.enableVideoSharpening {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private final class ItemLayout {
struct Grid {
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, 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 isDedicatedColumn {
if itemCount <= 2 {
itemsPerRow = 1
} else {
itemsPerRow = 2
}
} else {
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)
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 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
}
func contentHeight() -> CGFloat {
return self.frame(at: self.itemCount - 1).maxY
}
func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
if self.itemCount == 0 {
return (0, -1)
}
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
return (minVisibleIndex, maxVisibleIndex)
}
}
struct ExpandedGrid {
let containerSize: CGSize
let layout: Layout
let expandedInsets: UIEdgeInsets
let isUIHidden: Bool
init(containerSize: CGSize, layout: Layout, expandedInsets: UIEdgeInsets, isUIHidden: Bool) {
self.containerSize = containerSize
self.layout = layout
self.expandedInsets = expandedInsets
self.isUIHidden = isUIHidden
}
func itemContainerFrame() -> CGRect {
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()
}
}
}
struct List {
let containerSize: CGSize
let sideInset: CGFloat
let itemCount: Int
let itemHeight: CGFloat
let trailingItemHeights: [CGFloat]
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) {
self.containerSize = containerSize
self.sideInset = sideInset
self.itemCount = itemCount
self.itemHeight = itemHeight
self.trailingItemHeights = trailingItemHeights
}
func frame(at index: Int) -> CGRect {
let frame = CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.itemHeight))
return frame
}
func trailingItemFrame(index: Int) -> CGRect {
if index < 0 || index >= self.trailingItemHeights.count {
return CGRect()
}
var prefixHeight: CGFloat = 0.0
for i in 0 ..< index {
prefixHeight += self.trailingItemHeights[i]
}
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index]))
}
func contentHeight() -> CGFloat {
var result: CGFloat = 0.0
if self.itemCount != 0 {
result = self.frame(at: self.itemCount - 1).maxY
}
for height in self.trailingItemHeights {
result += height
}
return result
}
func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
if self.itemCount == 0 {
return (0, -1)
}
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) - 1)
return (minVisibleIndex, maxVisibleIndex)
}
}
let containerSize: CGSize
let layout: Layout
let isUIHidden: Bool
let expandedInsets: UIEdgeInsets
let safeInsets: UIEdgeInsets
let grid: Grid
let expandedGrid: ExpandedGrid
let list: List
let spacing: CGFloat
let gridOffsetY: CGFloat
let listOffsetY: CGFloat
let listFrame: CGRect
let separateVideoGridFrame: CGRect
let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: CGRect
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) {
self.containerSize = containerSize
self.layout = layout
self.isUIHidden = isUIHidden
self.expandedInsets = expandedInsets
self.safeInsets = safeInsets
let listWidth: CGFloat = layout.mainColumn.width
let gridWidth: CGFloat
let gridSideInset: CGFloat
let gridContainerHeight: CGFloat
if let videoColumn = layout.videoColumn {
if layout.isMainColumnHidden {
gridWidth = videoColumn.width + layout.columnSpacing + layout.mainColumn.width
} else {
gridWidth = videoColumn.width
}
gridSideInset = videoColumn.insets.left
gridContainerHeight = containerSize.height - videoColumn.insets.top - videoColumn.insets.bottom
} else {
gridWidth = listWidth
gridSideInset = layout.mainColumn.insets.left
gridContainerHeight = containerSize.height
}
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, trailingItemHeights: listTrailingItemHeights)
self.spacing = 4.0
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
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 layout.videoColumn == nil {
if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing
}
}
self.listOffsetY = listOffsetY
if let videoColumn = layout.videoColumn {
let columnsWidth: CGFloat = videoColumn.width + layout.columnSpacing + layout.mainColumn.width
let columnsLeftInset: CGFloat = layout.leftInset
var separateVideoGridFrame = CGRect(origin: CGPoint(x: columnsLeftInset, 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 || layout.isMainColumnHidden {
listFrame.origin.x = containerSize.width + columnsLeftInset
separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height))
}
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 + layout.mainColumn.insets.left, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth - layout.mainColumn.insets.left - layout.mainColumn.insets.right, height: containerSize.height - layout.mainColumn.insets.top))
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
if self.layout.videoColumn == nil {
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
result += self.spacing
}
}
result += self.list.contentHeight()
result += self.layout.mainColumn.insets.bottom
result += 24.0
return result
}
func separateVideoGridContentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY
if let videoColumn = self.layout.videoColumn {
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
}
result += videoColumn.insets.bottom
}
return result
}
func visibleGridItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
return self.grid.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.gridOffsetY))
}
func gridItemFrame(at index: Int) -> CGRect {
return self.grid.frame(at: index)
}
func gridItemContainerFrame() -> CGRect {
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()))
}
}
func visibleListItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
return self.list.visibleItemRange(for: rect.offsetBy(dx: 0.0, dy: -self.listOffsetY))
}
func listItemFrame(at index: Int) -> CGRect {
return self.list.frame(at: index)
}
func listItemContainerFrame() -> CGRect {
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()))
}
}
func listTrailingItemFrame(index: Int) -> CGRect {
return self.list.trailingItemFrame(index: index)
}
}
private struct ExpandedGridSwipeState {
var fraction: CGFloat
init(fraction: CGFloat) {
self.fraction = fraction
}
}
private final class VideoParticipant: Equatable {
let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool
var key: VideoParticipantKey {
return VideoParticipantKey(id: self.participant.id, isPresentation: self.isPresentation)
}
init(participant: GroupCallParticipantsContext.Participant, isPresentation: Bool) {
self.participant = participant
self.isPresentation = isPresentation
}
static func ==(lhs: VideoParticipant, rhs: VideoParticipant) -> Bool {
if lhs.participant != rhs.participant {
return false
}
if lhs.isPresentation != rhs.isPresentation {
return false
}
return true
}
}
private final class GridItem {
let key: VideoParticipantKey
let view = ComponentView<Empty>()
var isCollapsing: Bool = false
init(key: VideoParticipantKey) {
self.key = key
}
}
private final class ListItem {
let view = ComponentView<Empty>()
let separatorLayer = SimpleLayer()
init() {
}
}
final class View: UIView, UIScrollViewDelegate {
private let scrollViewClippingContainer: SolidRoundedCornersContainer
private let scrollView: ScrollView
private let scrollViewBottomShadowView: UIImageView
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
private let separateVideoScrollView: ScrollView
private(set) var component: VideoChatParticipantsComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var ignoreScrolling: Bool = false
private var gridParticipants: [VideoParticipant] = []
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
private let measureListItemView = ComponentView<Empty>()
private var inviteListItemViews: [Int: ComponentView<Empty>] = [:]
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
private let gridItemViewContainer: UIView
private let expandedGridItemContainer: UIView
private var expandedControlsView: ComponentView<Empty>?
private var expandedThumbnailsView: ComponentView<Empty>?
private var expandedSpeakingToast: ComponentView<Empty>?
private var listItemViews: [GroupCallParticipantsContext.Participant.Id: ListItem] = [:]
private let listItemViewContainer: UIView
private let listItemViewSeparatorContainer: SimpleLayer
private let listItemsBackground = ComponentView<Empty>()
private var itemLayout: ItemLayout?
private var expandedGridSwipeState: ExpandedGridSwipeState?
private var appliedGridIsEmpty: Bool = true
private var isPinchToZoomActive: Bool = false
private var stopRequestingNonCentralVideo: Bool = false
private var stopRequestingNonCentralVideoTimer: Foundation.Timer?
private var currentLoadMoreToken: String?
private var mainScrollViewEventCycleState: EventCycleState?
private var separateVideoScrollViewEventCycleState: EventCycleState?
override init(frame: CGRect) {
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
self.scrollView = ScrollView()
self.scrollViewBottomShadowView = UIImageView()
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
self.separateVideoScrollView = ScrollView()
self.gridItemViewContainer = UIView()
self.gridItemViewContainer.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.listItemViewContainer = UIView()
self.listItemViewContainer.clipsToBounds = true
self.listItemViewSeparatorContainer = SimpleLayer()
self.expandedGridItemContainer = UIView()
self.expandedGridItemContainer.clipsToBounds = true
super.init(frame: frame)
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.separateVideoScrollView.delaysContentTouches = false
self.separateVideoScrollView.canCancelContentTouches = true
self.separateVideoScrollView.clipsToBounds = false
self.separateVideoScrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.separateVideoScrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.separateVideoScrollView.showsVerticalScrollIndicator = false
self.separateVideoScrollView.showsHorizontalScrollIndicator = false
self.separateVideoScrollView.alwaysBounceHorizontal = false
self.separateVideoScrollView.alwaysBounceVertical = false
self.separateVideoScrollView.scrollsToTop = false
self.separateVideoScrollView.delegate = self
self.separateVideoScrollView.clipsToBounds = true
self.scrollViewClippingContainer.addSubview(self.scrollView)
self.addSubview(self.scrollViewClippingContainer)
self.addSubview(self.scrollViewClippingContainer.cornersView)
self.addSubview(self.scrollViewBottomShadowView)
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
self.addSubview(self.separateVideoScrollViewClippingContainer)
self.addSubview(self.separateVideoScrollViewClippingContainer.cornersView)
self.scrollView.addSubview(self.listItemViewContainer)
self.addSubview(self.expandedGridItemContainer)
self.expandedGridItemContainer.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.expandedGridPanGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stopRequestingNonCentralVideoTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else {
return nil
}
if component.expandedVideoState != nil {
if let result = self.expandedGridItemContainer.hitTest(self.convert(point, to: self.expandedGridItemContainer), with: event) {
return result
}
if component.layout.videoColumn != nil {
if let result = self.scrollViewClippingContainer.hitTest(self.convert(point, to: self.scrollViewClippingContainer), with: event) {
return result
}
}
if !self.expandedGridItemContainer.bounds.contains(self.convert(point, to: self.expandedGridItemContainer)) && !self.scrollViewClippingContainer.bounds.contains(self.convert(point, to: self.scrollViewClippingContainer)) {
return nil
}
return self
} else {
if let result = self.scrollViewClippingContainer.hitTest(self.convert(point, to: self.scrollViewClippingContainer), with: event) {
return result
} else if let result = self.separateVideoScrollViewClippingContainer.hitTest(self.convert(point, to: self.separateVideoScrollViewClippingContainer), with: event) {
return result
} else {
return nil
}
}
}
@objc private func expandedGridPanGesture(_ recognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
if self.bounds.height == 0.0 {
return
}
switch recognizer.state {
case .began, .changed:
let translation = recognizer.translation(in: self)
let fraction = translation.y / self.bounds.height
self.expandedGridSwipeState = ExpandedGridSwipeState(fraction: fraction)
self.state?.updated(transition: .immediate)
case .ended, .cancelled:
let translation = recognizer.translation(in: self)
let fraction = translation.y / self.bounds.height
self.expandedGridSwipeState = nil
let velocity = recognizer.velocity(in: self)
if abs(velocity.y) > 100.0 || abs(fraction) >= 0.5 {
component.updateMainParticipant(nil, nil)
} else {
self.state?.updated(transition: .spring(duration: 0.4))
}
default:
break
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
if scrollView == self.scrollView {
if let eventCycleState = self.mainScrollViewEventCycleState {
if eventCycleState.ignoreScrolling {
self.ignoreScrolling = true
scrollView.contentOffset = CGPoint()
self.ignoreScrolling = false
return
}
}
} else if scrollView == self.separateVideoScrollView {
if let eventCycleState = self.separateVideoScrollViewEventCycleState {
if eventCycleState.ignoreScrolling {
self.ignoreScrolling = true
scrollView.contentOffset = CGPoint()
self.ignoreScrolling = false
return
}
}
}
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if scrollView == self.scrollView {
if let eventCycleState = self.mainScrollViewEventCycleState {
if eventCycleState.ignoreScrolling {
targetContentOffset.pointee.y = 0.0
}
}
} else if scrollView == self.separateVideoScrollView {
if let eventCycleState = self.separateVideoScrollViewEventCycleState {
if eventCycleState.ignoreScrolling {
targetContentOffset.pointee.y = 0.0
}
}
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let itemLayout = self.itemLayout else {
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
var previousExpandedItemId: VideoParticipantKey?
for (key, item) in self.gridItemViews {
if item.view.view?.superview == self.expandedGridItemContainer {
previousExpandedItemId = key
break
}
}
let previousExpandedGridItemContainerFrame = self.expandedGridItemContainer.frame
var expandedGridItemContainerFrame: CGRect
if component.expandedVideoState != nil {
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
if let expandedGridSwipeState = self.expandedGridSwipeState {
expandedGridItemContainerFrame.origin.y += expandedGridSwipeState.fraction * itemLayout.containerSize.height
}
} else {
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 < 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
}
}
let commonGridItemTransition: ComponentTransition = (gridIsEmpty == gridWasEmpty) ? transition : .immediate
var validGridItemIds: [VideoParticipantKey] = []
var validGridItemIndices: [Int] = []
var clippedScrollViewBounds = self.scrollView.bounds
clippedScrollViewBounds.origin.y += component.layout.mainColumn.insets.top
clippedScrollViewBounds.size.height -= component.layout.mainColumn.insets.top + component.layout.mainColumn.insets.bottom
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
let clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int)
if itemLayout.layout.videoColumn == nil {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds)
} else {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
clippedVisibleGridItemRange = visibleGridItemRange
}
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
let videoParticipant = self.gridParticipants[index]
let videoParticipantKey = videoParticipant.key
validGridItemIds.append(videoParticipantKey)
validGridItemIndices.append(index)
}
}
if let expandedVideoState = component.expandedVideoState {
if !validGridItemIds.contains(expandedVideoState.mainParticipant), let index = self.gridParticipants.firstIndex(where: { $0.key == expandedVideoState.mainParticipant }) {
validGridItemIds.append(expandedVideoState.mainParticipant)
validGridItemIndices.append(index)
}
}
var visibleParticipants: [GroupCallParticipantsContext.Participant.Id] = []
for index in validGridItemIndices {
let videoParticipant = self.gridParticipants[index]
let videoParticipantKey = videoParticipant.key
validGridItemIds.append(videoParticipantKey)
var itemTransition = commonGridItemTransition
let itemView: GridItem
if let current = self.gridItemViews[videoParticipantKey] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = GridItem(key: videoParticipant.key)
self.gridItemViews[videoParticipantKey] = itemView
}
var isItemExpanded = false
var isItemUIHidden = false
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant == videoParticipantKey {
isItemExpanded = true
}
if expandedVideoState.isUIHidden {
isItemUIHidden = true
}
}
if isItemExpanded || (index >= clippedVisibleGridItemRange.minIndex && index <= clippedVisibleGridItemRange.maxIndex) {
visibleParticipants.append(videoParticipant.participant.id)
}
var suppressItemExpansionCollapseAnimation = false
if isItemExpanded {
if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey {
suppressItemExpansionCollapseAnimation = true
}
} else if component.expandedVideoState != nil {
if let previousExpandedItemId, previousExpandedItemId == videoParticipantKey {
suppressItemExpansionCollapseAnimation = true
}
}
var resultingItemTransition = commonGridItemTransition
if suppressItemExpansionCollapseAnimation {
itemTransition = itemTransition.withAnimation(.none)
resultingItemTransition = commonGridItemTransition.withAnimation(.none)
}
let itemFrame: CGRect
if isItemExpanded {
itemFrame = CGRect(origin: CGPoint(), size: itemLayout.expandedGrid.itemContainerFrame().size)
} else {
itemFrame = itemLayout.gridItemFrame(at: index)
}
let itemReferenceX: CGFloat = itemFrame.minX
let itemContainerWidth: CGFloat
if isItemExpanded {
itemContainerWidth = expandedGridItemContainerFrame.width
} else {
itemContainerWidth = itemLayout.grid.containerSize.width
}
let itemContentInsets: UIEdgeInsets
if isItemExpanded {
itemContentInsets = itemLayout.expandedGrid.itemContainerInsets()
} else {
itemContentInsets = UIEdgeInsets()
}
var itemControlInsets: UIEdgeInsets
if isItemExpanded {
itemControlInsets = itemContentInsets
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden {
} else {
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
}
var isSpeaking = false
if let participantPeer = videoParticipant.participant.peer {
isSpeaking = component.speakingParticipants.contains(participantPeer.id)
}
let _ = itemView.view.update(
transition: itemTransition,
component: AnyComponent(VideoChatParticipantVideoComponent(
theme: component.theme,
strings: component.strings,
call: component.call,
participant: videoParticipant.participant,
isMyPeer: videoParticipant.participant.peer?.id == component.participants?.myPeerId,
isPresentation: videoParticipant.isPresentation,
isSpeaking: isSpeaking,
maxVideoQuality: component.maxVideoQuality,
isExpanded: isItemExpanded,
isUIHidden: isItemUIHidden || self.isPinchToZoomActive,
contentInsets: itemContentInsets,
controlInsets: itemControlInsets,
interfaceOrientation: component.interfaceOrientation,
enableVideoSharpening: component.enableVideoSharpening,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if self.gridParticipants.count == 1, component.layout.videoColumn != nil {
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
component.updateMainParticipant(nil, false)
} else {
component.updateMainParticipant(videoParticipantKey, true)
}
} else {
if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey {
component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden)
} else {
component.updateMainParticipant(videoParticipantKey, nil)
}
}
},
contextAction: !isItemExpanded ? { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
} : nil,
activatePinch: isItemExpanded ? { [weak self] sourceNode in
guard let self, let component = self.component else {
return
}
self.isPinchToZoomActive = true
self.state?.updated(transition: .immediate, isLocal: true)
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
return UIScreen.main.bounds
})
component.call.accountContext.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController)
} : nil,
deactivatedPinch: isItemExpanded ? { [weak self] in
guard let self else {
return
}
self.isPinchToZoomActive = false
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
} : nil
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View {
if itemComponentView.superview == nil {
itemComponentView.layer.allowsGroupOpacity = true
if isItemExpanded {
if let expandedThumbnailsView = self.expandedThumbnailsView?.view {
self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView)
} else {
self.expandedGridItemContainer.addSubview(itemComponentView)
}
} else {
self.gridItemViewContainer.addSubview(itemComponentView)
}
itemComponentView.frame = itemFrame
itemComponentView.alpha = itemAlpha
itemComponentView.updateHorizontalReferenceLocation(containerWidth: itemContainerWidth, positionX: itemReferenceX, transition: .immediate)
if !resultingItemTransition.animation.isImmediate {
resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
}
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)
itemComponentView.center = fromFrame.center
if let expandedThumbnailsView = self.expandedThumbnailsView?.view {
self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView)
} else {
self.expandedGridItemContainer.addSubview(itemComponentView)
}
} else if !isItemExpanded && itemComponentView.superview != self.gridItemViewContainer {
if suppressItemExpansionCollapseAnimation {
self.gridItemViewContainer.addSubview(itemComponentView)
} else if !itemView.isCollapsing {
itemView.isCollapsing = true
let targetLocalItemFrame = itemLayout.gridItemFrame(at: index)
var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self)
targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY
targetItemFrame.origin.x -= expandedGridItemContainerFrame.minX
commonGridItemTransition.setPosition(view: itemComponentView, position: targetItemFrame.center)
commonGridItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: targetItemFrame.size), completion: { [weak self, weak itemView, weak itemComponentView] _ in
guard let self, let itemView, let itemComponentView else {
return
}
itemView.isCollapsing = false
self.gridItemViewContainer.addSubview(itemComponentView)
itemComponentView.center = targetLocalItemFrame.center
itemComponentView.bounds = CGRect(origin: CGPoint(), size: targetLocalItemFrame.size)
})
itemComponentView.updateHorizontalReferenceLocation(containerWidth: itemLayout.containerSize.width, positionX: itemFrame.minX, transition: commonGridItemTransition)
}
}
if !itemView.isCollapsing {
resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center)
resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
itemComponentView.updateHorizontalReferenceLocation(containerWidth: itemLayout.containerSize.width, positionX: itemFrame.minX, transition: resultingItemTransition)
let resultingItemAlphaTransition: ComponentTransition
if !resultingItemTransition.animation.isImmediate {
resultingItemAlphaTransition = alphaTransition
} else {
resultingItemAlphaTransition = .immediate
}
resultingItemAlphaTransition.setAlpha(view: itemComponentView, alpha: itemAlpha)
}
}
}
var removedGridItemIds: [VideoParticipantKey] = []
for (itemId, itemView) in self.gridItemViews {
if !validGridItemIds.contains(itemId) {
removedGridItemIds.append(itemId)
if let itemComponentView = itemView.view.view {
if !transition.animation.isImmediate {
if commonGridItemTransition.animation.isImmediate == transition.animation.isImmediate {
transition.setScale(view: itemComponentView, scale: 0.001)
}
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
} else {
itemComponentView.removeFromSuperview()
}
}
}
}
for itemId in removedGridItemIds {
self.gridItemViews.removeValue(forKey: itemId)
}
var validListItemIds: [GroupCallParticipantsContext.Participant.Id] = []
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds)
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
let itemFrame = itemLayout.listItemFrame(at: i)
let participantItemId: GroupCallParticipantsContext.Participant.Id
let peerItemComponent: PeerListItemComponent
if i < self.listParticipants.count {
let participant = self.listParticipants[i]
participantItemId = participant.id
let subtitle: PeerListItemComponent.Subtitle
if participant.id == .peer(component.call.accountContext.account.peerId) {
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_You, color: .accent)
} else if let participantPeer = participant.peer, component.speakingParticipants.contains(participantPeer.id) {
if let volume = participant.volume, volume / 100 != 100 {
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeakingVolume("\(volume / 100)%").string, color: .constructive)
} else {
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeaking, color: .constructive)
}
} else if let about = participant.about, !about.isEmpty {
subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral)
} else {
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusListening, color: .neutral)
}
var isSpeaking = false
if let participantPeer = participant.peer {
isSpeaking = component.speakingParticipants.contains(participantPeer.id)
}
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
muteState: participant.muteState,
hasRaiseHand: participant.hasRaiseHand,
isSpeaking: isSpeaking,
theme: component.theme
))
peerItemComponent = PeerListItemComponent(
context: component.call.accountContext,
theme: component.theme,
strings: component.strings,
style: .generic,
sideInset: 0.0,
title: participant.peer?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? "User \(participant.id)",
avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent(
call: component.call,
peer: participant.peer,
myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId,
isSpeaking: isSpeaking,
theme: component.theme
)),
peer: participant.peer,
subtitle: subtitle,
subtitleAccessory: .none,
presence: nil,
rightAccessoryComponent: AnyComponentWithIdentity(id: 0, component: rightAccessoryComponent),
selectionState: .none,
hasNext: false,
extractedTheme: PeerListItemComponent.ExtractedTheme(
inset: 2.0,
background: UIColor(white: 0.1, alpha: 1.0)
),
action: { [weak self] peer, _, itemView in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, itemView.extractedContainerView, nil)
},
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
}
)
} else {
let invitedPeer = component.invitedPeers[i - self.listParticipants.count]
participantItemId = .peer(invitedPeer.peer.id)
let subtitle: PeerListItemComponent.Subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusInvited, color: .neutral)
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantInvitedStatusComponent(
theme: component.theme
))
peerItemComponent = PeerListItemComponent(
context: component.call.accountContext,
theme: component.theme,
strings: component.strings,
style: .generic,
sideInset: 0.0,
title: invitedPeer.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent(
call: component.call,
peer: invitedPeer.peer,
myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId,
isSpeaking: false,
theme: component.theme
)),
peer: invitedPeer.peer,
subtitle: subtitle,
subtitleAccessory: .none,
presence: nil,
rightAccessoryComponent: AnyComponentWithIdentity(id: 1, component: rightAccessoryComponent),
selectionState: .none,
hasNext: false,
extractedTheme: PeerListItemComponent.ExtractedTheme(
inset: 2.0,
background: UIColor(white: 0.1, alpha: 1.0)
),
action: { [weak self] peer, _, itemView in
guard let self, let component = self.component else {
return
}
component.openInvitedParticipantContextMenu(peer.id, itemView.extractedContainerView, nil)
},
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openInvitedParticipantContextMenu(peer.id, sourceView, gesture)
}
)
}
validListItemIds.append(participantItemId)
if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex {
visibleParticipants.append(participantItemId)
}
var itemTransition = transition
let itemView: ListItem
if let current = self.listItemViews[participantItemId] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ListItem()
self.listItemViews[participantItemId] = itemView
}
let _ = itemView.view.update(
transition: itemTransition,
component: AnyComponent(peerItemComponent),
environment: {},
containerSize: itemFrame.size
)
let itemSeparatorFrame = CGRect(origin: CGPoint(x: itemFrame.minX + 63.0, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: itemFrame.width - 63.0, height: UIScreenPixel))
if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil {
itemComponentView.clipsToBounds = true
itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.blitOver(UIColor(white: 0.1, alpha: 1.0), alpha: 1.0).cgColor
self.listItemViewContainer.addSubview(itemComponentView)
self.listItemViewSeparatorContainer.addSublayer(itemView.separatorLayer)
if !transition.animation.isImmediate {
itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
itemComponentView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: 0.0))
var startingItemSeparatorFrame = itemSeparatorFrame
startingItemSeparatorFrame.origin.y = itemFrame.minY - UIScreenPixel
itemView.separatorLayer.frame = startingItemSeparatorFrame
}
}
transition.setFrame(view: itemComponentView, frame: itemFrame)
transition.setFrame(layer: itemView.separatorLayer, frame: itemSeparatorFrame)
}
}
}
var removedListItemIds: [GroupCallParticipantsContext.Participant.Id] = []
for (itemId, itemView) in self.listItemViews {
if !validListItemIds.contains(itemId) {
removedListItemIds.append(itemId)
if let itemComponentView = itemView.view.view {
let itemSeparatorLayer = itemView.separatorLayer
if !transition.animation.isImmediate {
var itemFrame = itemComponentView.frame
itemFrame.size.height = 0.0
transition.setFrame(view: itemComponentView, frame: itemFrame)
var itemSeparatorFrame = itemSeparatorLayer.frame
itemSeparatorFrame.origin.y = itemFrame.minY - UIScreenPixel
transition.setFrame(layer: itemSeparatorLayer, frame: itemSeparatorFrame, completion: { [weak itemSeparatorLayer] _ in
itemSeparatorLayer?.removeFromSuperlayer()
})
itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
} else {
itemComponentView.removeFromSuperview()
itemSeparatorLayer.removeFromSuperlayer()
}
}
}
}
for itemId in removedListItemIds {
self.listItemViews.removeValue(forKey: itemId)
}
var trailingItemIndex = 0
for inviteOption in component.participants?.inviteOptions ?? [] {
guard let itemView = self.inviteListItemViews[inviteOption.id] else {
continue
}
var itemTransition = transition
let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex)
trailingItemIndex += 1
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
itemTransition = itemTransition.withAnimation(.none)
self.listItemViewContainer.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
var removeInviteListItemIds: [Int] = []
for (id, itemView) in self.inviteListItemViews {
if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) {
} else {
removeInviteListItemIds.append(id)
itemView.view?.removeFromSuperview()
}
}
for id in removeInviteListItemIds {
self.inviteListItemViews.removeValue(forKey: id)
}
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
transition.setBounds(view: self.gridItemViewContainer, bounds: CGRect(origin: CGPoint(), size: itemLayout.gridItemContainerFrame().size))
transition.setFrame(view: self.listItemViewContainer, frame: itemLayout.listItemContainerFrame())
transition.setFrame(layer: self.listItemViewSeparatorContainer, frame: CGRect(origin: CGPoint(), size: itemLayout.listItemContainerFrame().size))
if self.expandedGridItemContainer.frame != expandedGridItemContainerFrame {
self.expandedGridItemContainer.layer.cornerRadius = 10.0
transition.setFrame(view: self.expandedGridItemContainer, frame: expandedGridItemContainerFrame, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.expandedGridItemContainer.layer.cornerRadius = 0.0
})
}
if let expandedVideoState = component.expandedVideoState {
var thumbnailParticipants: [VideoChatExpandedParticipantThumbnailsComponent.Participant] = []
for participant in self.gridParticipants {
thumbnailParticipants.append(VideoChatExpandedParticipantThumbnailsComponent.Participant(
participant: participant.participant,
isPresentation: participant.isPresentation
))
}
let expandedControlsAlpha: CGFloat = (expandedVideoState.isUIHidden || self.isPinchToZoomActive) ? 0.0 : 1.0
let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha
var expandedThumbnailsTransition = transition
let expandedThumbnailsView: ComponentView<Empty>
if let current = self.expandedThumbnailsView {
expandedThumbnailsView = current
} else {
expandedThumbnailsTransition = expandedThumbnailsTransition.withAnimation(.none)
expandedThumbnailsView = ComponentView()
self.expandedThumbnailsView = expandedThumbnailsView
}
let expandedThumbnailsSize = expandedThumbnailsView.update(
transition: expandedThumbnailsTransition,
component: AnyComponent(VideoChatExpandedParticipantThumbnailsComponent(
call: component.call,
theme: component.theme,
displayVideo: component.maxVideoQuality != 0,
participants: thumbnailParticipants,
selectedParticipant: component.expandedVideoState.flatMap { expandedVideoState in
return VideoChatExpandedParticipantThumbnailsComponent.Participant.Key(id: expandedVideoState.mainParticipant.id, isPresentation: expandedVideoState.mainParticipant.isPresentation)
},
speakingParticipants: component.speakingParticipants,
interfaceOrientation: component.interfaceOrientation,
updateSelectedParticipant: { [weak self] key in
guard let self, let component = self.component else {
return
}
component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil)
},
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
}
)),
environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
)
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
if expandedVideoState.isUIHidden {
expandedThumbnailsFrame.origin.y += expandedThumbnailsSize.height
}
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.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) {
fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer)
} else {
fromReferenceFrame = previousExpandedGridItemContainerFrame
}
expandedThumbnailsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.maxY - expandedThumbnailsSize.height), size: expandedThumbnailsFrame.size)
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
let expandedControlsView: ComponentView<Empty>
if let current = self.expandedControlsView {
expandedControlsView = current
} else {
expandedControlsTransition = expandedControlsTransition.withAnimation(.none)
expandedControlsView = ComponentView()
self.expandedControlsView = expandedControlsView
}
let expandedControlsSize = expandedControlsView.update(
transition: expandedControlsTransition,
component: AnyComponent(VideoChatExpandedControlsComponent(
theme: component.theme,
strings: component.strings,
isPinned: expandedVideoState.isMainParticipantPinned,
backAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.updateMainParticipant(nil, nil)
},
pinAction: { [weak self] in
guard let self, let component = self.component else {
return
}
guard let expandedVideoState = component.expandedVideoState else {
return
}
component.updateIsMainParticipantPinned(!expandedVideoState.isMainParticipantPinned)
}
)),
environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
)
let expandedControlsFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedControlsSize)
if let expandedControlsComponentView = expandedControlsView.view {
if expandedControlsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedControlsComponentView)
expandedControlsComponentView.alpha = expandedControlsAlpha
let fromReferenceFrame: CGRect
if let index = self.gridParticipants.firstIndex(where: { $0.participant.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) {
fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer)
} else {
fromReferenceFrame = previousExpandedGridItemContainerFrame
}
expandedControlsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.minY - previousExpandedGridItemContainerFrame.minY), size: expandedControlsFrame.size)
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 {
self.expandedThumbnailsView = nil
if transition.containedViewLayoutTransition.isAnimated, let expandedThumbnailsComponentView = expandedThumbnailsView.view {
if let collapsingItemView = self.gridItemViews.values.first(where: { $0.isCollapsing }), let index = self.gridParticipants.firstIndex(where: { $0.participant.id == collapsingItemView.key.id && $0.isPresentation == collapsingItemView.key.isPresentation }) {
let targetLocalItemFrame = itemLayout.gridItemFrame(at: index)
var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self)
targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY
targetItemFrame.origin.x -= expandedGridItemContainerFrame.minX
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: expandedThumbnailsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in
expandedThumbnailsComponentView?.removeFromSuperview()
})
} else {
expandedThumbnailsView.view?.removeFromSuperview()
}
}
if let expandedControlsView = self.expandedControlsView {
self.expandedControlsView = nil
if transition.containedViewLayoutTransition.isAnimated, let expandedControlsComponentView = expandedControlsView.view {
if let collapsingItemView = self.gridItemViews.values.first(where: { $0.isCollapsing }), let index = self.gridParticipants.firstIndex(where: { $0.participant.id == collapsingItemView.key.id && $0.isPresentation == collapsingItemView.key.isPresentation }) {
let targetLocalItemFrame = itemLayout.gridItemFrame(at: index)
var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self)
targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY
targetItemFrame.origin.x -= expandedGridItemContainerFrame.minX
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: expandedControlsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in
expandedControlsComponentView?.removeFromSuperview()
})
} else {
expandedControlsView.view?.removeFromSuperview()
}
}
}
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { expandedVideoState.mainParticipant.id != .peer($0) }), let speakingPeer = participants.participants.first(where: { $0.id == .peer(firstOther) })?.peer {
let expandedSpeakingToast: ComponentView<Empty>
var expandedSpeakingToastTransition = transition
if let current = self.expandedSpeakingToast {
expandedSpeakingToast = current
} else {
expandedSpeakingToastTransition = expandedSpeakingToastTransition.withAnimation(.none)
expandedSpeakingToast = ComponentView()
self.expandedSpeakingToast = expandedSpeakingToast
}
let expandedSpeakingToastSize = expandedSpeakingToast.update(
transition: expandedSpeakingToastTransition,
component: AnyComponent(VideoChatExpandedSpeakingToastComponent(
context: component.call.accountContext,
peer: speakingPeer,
strings: component.strings,
theme: component.theme,
action: { [weak self] peer in
guard let self, let component = self.component, let participants = component.participants else {
return
}
guard let participant = participants.participants.first(where: { $0.id == .peer(peer.id) }) else {
return
}
var key: VideoParticipantKey?
if participant.presentationDescription != nil {
key = VideoParticipantKey(id: .peer(peer.id), isPresentation: true)
} else if participant.videoDescription != nil {
key = VideoParticipantKey(id: .peer(peer.id), isPresentation: false)
}
if let key {
component.updateMainParticipant(key, nil)
}
}
)),
environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
)
let expandedSpeakingToastFrame = CGRect(origin: CGPoint(x: floor((itemLayout.expandedGrid.itemContainerFrame().size.width - expandedSpeakingToastSize.width) * 0.5), y: 44.0), size: expandedSpeakingToastSize)
if let expandedSpeakingToastView = expandedSpeakingToast.view {
var animateIn = false
if expandedSpeakingToastView.superview == nil {
animateIn = true
self.expandedGridItemContainer.addSubview(expandedSpeakingToastView)
}
expandedSpeakingToastTransition.setFrame(view: expandedSpeakingToastView, frame: expandedSpeakingToastFrame)
if animateIn {
alphaTransition.animateAlpha(view: expandedSpeakingToastView, from: 0.0, to: 1.0)
transition.animateScale(view: expandedSpeakingToastView, from: 0.6, to: 1.0)
}
}
} else {
if let expandedSpeakingToast = self.expandedSpeakingToast {
self.expandedSpeakingToast = nil
if let expandedSpeakingToastView = expandedSpeakingToast.view {
alphaTransition.setAlpha(view: expandedSpeakingToastView, alpha: 0.0, completion: { [weak expandedSpeakingToastView] _ in
expandedSpeakingToastView?.removeFromSuperview()
})
transition.setScale(view: expandedSpeakingToastView, scale: 0.6)
}
}
}
if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 {
if self.currentLoadMoreToken != loadMoreToken {
self.currentLoadMoreToken = loadMoreToken
component.call.loadMoreMembers(token: loadMoreToken)
}
}
component.visibleParticipantsUpdated(Set(visibleParticipants.compactMap {
if case let .peer(id) = $0 {
return id
} else {
return nil
}
}))
}
func setEventCycleState(scrollView: UIScrollView, eventCycleState: EventCycleState?) {
if scrollView == self.scrollView {
self.mainScrollViewEventCycleState = eventCycleState
} else if scrollView == self.separateVideoScrollView {
self.separateVideoScrollViewEventCycleState = eventCycleState
}
}
func itemFrame(peerId: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool) -> CGRect? {
for (key, itemView) in self.gridItemViews {
if key.id == peerId && key.isPresentation == isPresentation {
if let itemComponentView = itemView.view.view {
return itemComponentView.convert(itemComponentView.bounds, to: self)
}
}
}
return nil
}
func updateItemPlaceholder(peerId: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool, placeholder: VideoSource.Output) {
for (key, itemView) in self.gridItemViews {
if key.id == peerId && key.isPresentation == isPresentation {
if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View {
itemComponentView.updatePlaceholder(placeholder: placeholder)
}
}
}
}
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let previousComponent = self.component
let _ = previousComponent
self.component = component
self.state = state
/*if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden {
if self.stopRequestingNonCentralVideoTimer == nil || previousComponent?.expandedVideoState != expandedVideoState {
self.stopRequestingNonCentralVideoTimer?.invalidate()
self.stopRequestingNonCentralVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.stopRequestingNonCentralVideo = true
self.stopRequestingNonCentralVideoTimer = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
})
}
} else {
self.stopRequestingNonCentralVideo = false
if let stopRequestingNonCentralVideoTimer = self.stopRequestingNonCentralVideoTimer {
self.stopRequestingNonCentralVideoTimer = nil
stopRequestingNonCentralVideoTimer.invalidate()
}
}*/
var gridParticipants: [VideoParticipant] = []
var listParticipants: [GroupCallParticipantsContext.Participant] = []
if let participants = component.participants {
for participant in participants.participants {
var isFullyMuted = false
if let muteState = participant.muteState, !muteState.canUnmute {
isFullyMuted = true
}
var hasVideo = false
if participant.videoDescription != nil {
hasVideo = true
let videoParticipant = VideoParticipant(participant: participant, isPresentation: false)
if participant.id == .peer(participants.myPeerId) {
gridParticipants.insert(videoParticipant, at: 0)
} else {
gridParticipants.append(videoParticipant)
}
}
if participant.presentationDescription != nil {
hasVideo = true
let videoParticipant = VideoParticipant(participant: participant, isPresentation: true)
if participant.id == .peer(participants.myPeerId) {
gridParticipants.insert(videoParticipant, at: 0)
} else {
gridParticipants.append(videoParticipant)
}
}
if !hasVideo || component.layout.videoColumn != nil {
if participant.id == .peer(participants.myPeerId) && !isFullyMuted {
listParticipants.insert(participant, at: 0)
} else {
listParticipants.append(participant)
}
}
}
}
self.gridParticipants = gridParticipants
self.listParticipants = listParticipants
let measureListItemSize = self.measureListItemView.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
context: component.call.accountContext,
theme: component.theme,
strings: component.strings,
style: .generic,
sideInset: 0.0,
title: "AAA",
peer: nil,
subtitle: PeerListItemComponent.Subtitle(text: "bbb", color: .neutral),
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: true,
action: { _, _, _ in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
var inviteListItemSizes: [CGSize] = []
if let participants = component.participants {
let tempItemLayout = ItemLayout(
containerSize: availableSize,
layout: component.layout,
isUIHidden: component.expandedVideoState?.isUIHidden ?? false,
expandedInsets: component.expandedInsets,
safeInsets: component.safeInsets,
gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count + component.invitedPeers.count,
listItemHeight: measureListItemSize.height,
listTrailingItemHeights: []
)
for i in 0 ..< participants.inviteOptions.count {
let inviteOption = participants.inviteOptions[i]
let inviteText: String
let iconType: VideoChatListInviteComponent.Icon
switch inviteOption.type {
case let .invite(isMultiple):
if isMultiple {
inviteText = component.strings.VoiceChat_InviteMember
} else {
inviteText = component.strings.VideoChat_InviteMember
}
iconType = .addUser
case .shareLink:
inviteText = component.strings.VoiceChat_Share
iconType = .link
}
let inviteListItemView: ComponentView<Empty>
var inviteListItemTransition = transition
if let current = self.inviteListItemViews[inviteOption.id] {
inviteListItemView = current
} else {
inviteListItemView = ComponentView()
self.inviteListItemViews[inviteOption.id] = inviteListItemView
inviteListItemTransition = inviteListItemTransition.withAnimation(.none)
}
inviteListItemSizes.append(inviteListItemView.update(
transition: inviteListItemTransition,
component: AnyComponent(VideoChatListInviteComponent(
title: inviteText,
icon: iconType,
theme: component.theme,
hasNext: i != participants.inviteOptions.count - 1,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.openInviteMembers(inviteOption.type)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - tempItemLayout.list.sideInset * 2.0, height: 1000.0)
))
}
}
let itemLayout = ItemLayout(
containerSize: availableSize,
layout: component.layout,
isUIHidden: component.expandedVideoState?.isUIHidden ?? false,
expandedInsets: component.expandedInsets,
safeInsets: component.safeInsets,
gridItemCount: gridParticipants.count,
listItemCount: listParticipants.count + component.invitedPeers.count,
listItemHeight: measureListItemSize.height,
listTrailingItemHeights: inviteListItemSizes.map(\.height)
)
self.itemLayout = itemLayout
let listItemsBackgroundSize = self.listItemsBackground.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
color: UIColor(white: 0.1, alpha: 1.0),
cornerRadius: 10.0
)),
environment: {},
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: itemLayout.layout.mainColumn.insets.left, y: 0.0), size: listItemsBackgroundSize)
if let listItemsBackgroundView = self.listItemsBackground.view {
if listItemsBackgroundView.superview == nil {
self.listItemViewContainer.addSubview(listItemsBackgroundView)
self.listItemViewContainer.layer.addSublayer(self.listItemViewSeparatorContainer)
}
transition.setFrame(view: listItemsBackgroundView, frame: listItemsBackgroundFrame)
}
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
if let participants = component.participants, component.maxVideoQuality != 0 {
for participant in participants.participants {
var maxVideoQuality: PresentationGroupCallRequestedVideo.Quality = .medium
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant.id == participant.id, !expandedVideoState.mainParticipant.isPresentation {
if component.maxVideoQuality == Int.max {
maxVideoQuality = .full
} else if component.maxVideoQuality == 360 {
maxVideoQuality = .medium
} else {
maxVideoQuality = .thumbnail
}
} else {
maxVideoQuality = .thumbnail
}
}
var maxPresentationQuality: PresentationGroupCallRequestedVideo.Quality = .medium
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant.id == participant.id, expandedVideoState.mainParticipant.isPresentation {
if component.maxVideoQuality == Int.max {
maxVideoQuality = .full
} else if component.maxVideoQuality == 360 {
maxVideoQuality = .medium
} else {
maxVideoQuality = .thumbnail
}
} else {
maxPresentationQuality = .thumbnail
}
}
if component.layout.videoColumn != nil && gridParticipants.count == 1 {
if component.maxVideoQuality == Int.max {
maxVideoQuality = .full
} else if component.maxVideoQuality == 360 {
maxVideoQuality = .medium
} else {
maxVideoQuality = .thumbnail
}
maxPresentationQuality = maxVideoQuality
}
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxVideoQuality != .full {
} else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) {
if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxPresentationQuality != .full {
} else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
}
}
component.call.setRequestedVideoList(items: requestedVideo)
transition.setPosition(view: self.scrollViewClippingContainer, position: itemLayout.scrollClippingFrame.center)
transition.setBounds(view: self.scrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX - itemLayout.listFrame.minX, y: itemLayout.scrollClippingFrame.minY - itemLayout.listFrame.minY), size: itemLayout.scrollClippingFrame.size))
transition.setFrame(view: self.scrollViewClippingContainer.cornersView, frame: itemLayout.scrollClippingFrame)
self.scrollViewClippingContainer.update(params: SolidRoundedCornersContainer.Params(
size: itemLayout.scrollClippingFrame.size,
color: .black,
cornerRadius: 10.0,
smoothCorners: false
), transition: transition)
if self.scrollViewBottomShadowView.image == nil {
let height: CGFloat = 80.0
let baseGradientAlpha: CGFloat = 1.0
let numSteps = 8
let firstStep = 0
let firstLocation = 0.0
let colors = (0 ..< numSteps).map { i -> UIColor in
if i < firstStep {
return UIColor(white: 1.0, alpha: 1.0)
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
let value: CGFloat = 1.0 - Display.bezierPoint(0.42, 0.0, 0.58, 1.0, step)
return UIColor(white: 0.0, alpha: baseGradientAlpha * value)
}
}
let locations = (0 ..< numSteps).map { i -> CGFloat in
if i < firstStep {
return 0.0
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
return (firstLocation + (1.0 - firstLocation) * step)
}
}
self.scrollViewBottomShadowView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0))
self.scrollViewBottomShadowView.tintColor = .black
}
let scrollViewBottomShadowOverflow: CGFloat = 30.0
let scrollViewBottomShadowFrame = CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX, y: itemLayout.scrollClippingFrame.maxY - component.layout.mainColumn.insets.bottom - scrollViewBottomShadowOverflow), size: CGSize(width: itemLayout.scrollClippingFrame.width, height: component.layout.mainColumn.insets.bottom + scrollViewBottomShadowOverflow))
transition.setFrame(view: self.scrollViewBottomShadowView, frame: scrollViewBottomShadowFrame)
transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center)
transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size))
transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame)
self.separateVideoScrollViewClippingContainer.update(params: SolidRoundedCornersContainer.Params(
size: itemLayout.separateVideoScrollClippingFrame.size,
color: .black,
cornerRadius: 10.0,
smoothCorners: false
), transition: transition)
self.ignoreScrolling = true
if self.scrollView.bounds.size != itemLayout.listFrame.size {
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: itemLayout.listFrame.size))
}
let contentSize = CGSize(width: itemLayout.listFrame.width, height: itemLayout.contentHeight())
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
if self.separateVideoScrollView.bounds.size != itemLayout.separateVideoGridFrame.size {
transition.setFrame(view: self.separateVideoScrollView, frame: CGRect(origin: CGPoint(), size: itemLayout.separateVideoGridFrame.size))
}
let separateVideoContentSize = CGSize(width: itemLayout.separateVideoGridFrame.width, height: itemLayout.separateVideoGridContentHeight())
if self.separateVideoScrollView.contentSize != separateVideoContentSize {
self.separateVideoScrollView.contentSize = separateVideoContentSize
}
self.ignoreScrolling = false
if itemLayout.layout.videoColumn == nil {
if self.gridItemViewContainer.superview !== self.scrollView {
self.scrollView.addSubview(self.gridItemViewContainer)
}
} else {
if self.gridItemViewContainer.superview !== self.separateVideoScrollView {
self.separateVideoScrollView.addSubview(self.gridItemViewContainer)
}
}
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}