mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
2109 lines
112 KiB
Swift
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)
|
|
}
|
|
}
|