[WIP] Video chats

This commit is contained in:
Isaac 2024-09-06 21:41:16 +08:00
parent 3aceee1696
commit ccedf9fc06
9 changed files with 1533 additions and 147 deletions

View File

@ -471,7 +471,12 @@ public struct ComponentTransition {
layer.removeAnimation(forKey: "opacity")
completion?(true)
case .curve:
let previousAlpha = layer.presentation()?.opacity ?? layer.opacity
let previousAlpha: Float
if layer.animation(forKey: "opacity") != nil {
previousAlpha = layer.presentation()?.opacity ?? layer.opacity
} else {
previousAlpha = layer.opacity
}
layer.opacity = Float(alpha)
self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, delay: delay, completion: completion)
}

View File

@ -10,16 +10,22 @@ import BackButtonComponent
final class VideoChatExpandedControlsComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let isPinned: Bool
let backAction: () -> Void
let pinAction: () -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
backAction: @escaping () -> Void
isPinned: Bool,
backAction: @escaping () -> Void,
pinAction: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.isPinned = isPinned
self.backAction = backAction
self.pinAction = pinAction
}
static func ==(lhs: VideoChatExpandedControlsComponent, rhs: VideoChatExpandedControlsComponent) -> Bool {
@ -29,11 +35,15 @@ final class VideoChatExpandedControlsComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.isPinned != rhs.isPinned {
return false
}
return true
}
final class View: UIView {
private let backButton = ComponentView<Empty>()
private let pinStatus = ComponentView<Empty>()
private var component: VideoChatExpandedControlsComponent?
private var isUpdating: Bool = false
@ -52,6 +62,9 @@ final class VideoChatExpandedControlsComponent: Component {
if let backButtonView = self.backButton.view, let result = backButtonView.hitTest(self.convert(point, to: backButtonView), with: event) {
return result
}
if let pinStatusView = self.pinStatus.view, let result = pinStatusView.hitTest(self.convert(point, to: pinStatusView), with: event) {
return result
}
return nil
}
@ -86,6 +99,30 @@ final class VideoChatExpandedControlsComponent: Component {
transition.setFrame(view: backButtonView, frame: backButtonFrame)
}
let pinStatusSize = self.pinStatus.update(
transition: transition,
component: AnyComponent(VideoChatPinStatusComponent(
theme: component.theme,
strings: component.strings,
isPinned: component.isPinned,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.pinAction()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let pinStatusFrame = CGRect(origin: CGPoint(x: availableSize.width - 0.0 - pinStatusSize.width, y: 0.0), size: pinStatusSize)
if let pinStatusView = self.pinStatus.view {
if pinStatusView.superview == nil {
self.addSubview(pinStatusView)
}
transition.setFrame(view: pinStatusView, frame: pinStatusFrame)
}
return availableSize
}
}

View File

@ -10,6 +10,7 @@ import CallScreen
import MetalEngine
import SwiftSignalKit
import AccountContext
import RadialStatusNode
private final class BlobView: UIView {
let blobsLayer: CallBlobsLayer
@ -209,6 +210,8 @@ final class VideoChatMicButtonComponent: Component {
final class View: HighlightTrackingButton {
private let background: UIImageView
private var disappearingBackgrounds: [UIImageView] = []
private var progressIndicator: RadialStatusNode?
private let title = ComponentView<Empty>()
private let icon: VoiceChatActionButtonIconNode
@ -330,8 +333,38 @@ final class VideoChatMicButtonComponent: Component {
self.addSubview(self.background)
self.background.frame = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0))
}
transition.setPosition(view: self.background, position: CGRect(origin: CGPoint(), size: size).center)
transition.setScale(view: self.background, scale: size.width / 116.0)
if case .connecting = component.content {
let progressIndicator: RadialStatusNode
if let current = self.progressIndicator {
progressIndicator = current
} else {
progressIndicator = RadialStatusNode(backgroundNodeColor: .clear)
self.progressIndicator = progressIndicator
}
progressIndicator.transitionToState(.progress(color: UIColor(rgb: 0x0080FF), lineWidth: 3.0, value: nil, cancelEnabled: false, animateRotation: true))
let progressIndicatorView = progressIndicator.view
if progressIndicatorView.superview == nil {
self.addSubview(progressIndicatorView)
progressIndicatorView.center = CGRect(origin: CGPoint(), size: size).center
progressIndicatorView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0))
progressIndicatorView.layer.transform = CATransform3DMakeScale(size.width / 116.0, size.width / 116.0, 1.0)
} else {
transition.setPosition(view: progressIndicatorView, position: CGRect(origin: CGPoint(), size: size).center)
transition.setScale(view: progressIndicatorView, scale: size.width / 116.0)
}
} else if let progressIndicator = self.progressIndicator {
self.progressIndicator = nil
if !transition.animation.isImmediate {
let progressIndicatorView = progressIndicator.view
progressIndicatorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak progressIndicatorView] _ in
progressIndicatorView?.removeFromSuperview()
})
} else {
progressIndicator.view.removeFromSuperview()
}
}
if previousComponent?.content != component.content {
let backgroundContentsTransition: ComponentTransition
@ -340,7 +373,7 @@ final class VideoChatMicButtonComponent: Component {
} else {
backgroundContentsTransition = .immediate
}
let backgroundImage = generateImage(CGSize(width: 116.0, height: 116.0), rotatedContext: { size, context in
let backgroundImage = generateImage(CGSize(width: 200.0, height: 200.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addEllipse(in: CGRect(origin: CGPoint(), size: size))
context.clip()
@ -366,11 +399,41 @@ final class VideoChatMicButtonComponent: Component {
}
})!
if let previousImage = self.background.image {
let previousBackground = UIImageView()
previousBackground.center = self.background.center
previousBackground.bounds = self.background.bounds
previousBackground.layer.transform = self.background.layer.transform
previousBackground.image = previousImage
self.insertSubview(previousBackground, aboveSubview: self.background)
self.disappearingBackgrounds.append(previousBackground)
self.background.image = backgroundImage
backgroundContentsTransition.animateContentsImage(layer: self.background.layer, from: previousImage.cgImage!, to: backgroundImage.cgImage!, duration: 0.2, curve: .easeInOut)
backgroundContentsTransition.setAlpha(view: previousBackground, alpha: 0.0, completion: { [weak self, weak previousBackground] _ in
guard let self, let previousBackground else {
return
}
previousBackground.removeFromSuperview()
self.disappearingBackgrounds.removeAll(where: { $0 === previousBackground })
})
} else {
self.background.image = backgroundImage
}
if !transition.animation.isImmediate, let previousComponent, case .connecting = previousComponent.content {
self.layer.animateSublayerScale(from: 1.0, to: 1.07, duration: 0.12, removeOnCompletion: false, completion: { [weak self] completed in
if let self, completed {
self.layer.removeAnimation(forKey: "sublayerTransform.scale")
self.layer.animateSublayerScale(from: 1.07, to: 1.0, duration: 0.12, removeOnCompletion: true)
}
})
}
}
transition.setPosition(view: self.background, position: CGRect(origin: CGPoint(), size: size).center)
transition.setScale(view: self.background, scale: size.width / 116.0)
for disappearingBackground in self.disappearingBackgrounds {
transition.setPosition(view: disappearingBackground, position: CGRect(origin: CGPoint(), size: size).center)
transition.setScale(view: disappearingBackground, scale: size.width / 116.0)
}
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
@ -434,6 +497,8 @@ final class VideoChatMicButtonComponent: Component {
blobTintTransition.setTintColor(layer: blobView.blobsLayer, color: component.content == .muted ? UIColor(rgb: 0x0086FF) : UIColor(rgb: 0x33C758))
switch component.content {
case .unmuted:
if self.audioLevelDisposable == nil {
self.audioLevelDisposable = (component.call.myAudioLevel
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
@ -443,6 +508,13 @@ final class VideoChatMicButtonComponent: Component {
blobView.updateLevel(CGFloat(value), immediately: false)
})
}
case .connecting, .muted:
if let audioLevelDisposable = self.audioLevelDisposable {
self.audioLevelDisposable = nil
audioLevelDisposable.dispose()
blobView.updateLevel(0.0, immediately: false)
}
}
var glowFrame = CGRect(origin: CGPoint(), size: availableSize)
if component.isCollapsed {

View File

@ -41,6 +41,7 @@ final class VideoChatParticipantVideoComponent: Component {
let isSpeaking: Bool
let isExpanded: Bool
let bottomInset: CGFloat
weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
let action: (() -> Void)?
init(
@ -50,6 +51,7 @@ final class VideoChatParticipantVideoComponent: Component {
isSpeaking: Bool,
isExpanded: Bool,
bottomInset: CGFloat,
rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?,
action: (() -> Void)?
) {
self.call = call
@ -58,6 +60,7 @@ final class VideoChatParticipantVideoComponent: Component {
self.isSpeaking = isSpeaking
self.isExpanded = isExpanded
self.bottomInset = bottomInset
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
self.action = action
}
@ -113,6 +116,8 @@ final class VideoChatParticipantVideoComponent: Component {
private var activityBorderView: UIImageView?
private var loadingEffectView: PortalView?
override init(frame: CGRect) {
super.init(frame: frame)
@ -429,6 +434,18 @@ final class VideoChatParticipantVideoComponent: Component {
self.videoSpec = nil
}
if self.loadingEffectView == nil, let rootVideoLoadingEffectView = component.rootVideoLoadingEffectView {
if let loadingEffectView = PortalView(matchPosition: true) {
self.loadingEffectView = loadingEffectView
self.addSubview(loadingEffectView.view)
rootVideoLoadingEffectView.portalSource.addPortal(view: loadingEffectView)
loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize)
}
}
if let loadingEffectView = self.loadingEffectView {
transition.setFrame(view: loadingEffectView.view, frame: CGRect(origin: CGPoint(), size: availableSize))
}
if component.isSpeaking && !component.isExpanded {
let activityBorderView: UIImageView
if let current = self.activityBorderView {

View File

@ -12,6 +12,23 @@ import TelegramPresentationData
import PeerListItemComponent
final class VideoChatParticipantsComponent: Component {
enum LayoutType: Equatable {
struct Horizontal: Equatable {
var rightColumnWidth: CGFloat
var columnSpacing: CGFloat
var isCentered: Bool
init(rightColumnWidth: CGFloat, columnSpacing: CGFloat, isCentered: Bool) {
self.rightColumnWidth = rightColumnWidth
self.columnSpacing = columnSpacing
self.isCentered = isCentered
}
}
case vertical
case horizontal(Horizontal)
}
final class Participants: Equatable {
let myPeerId: EnginePeer.Id
let participants: [GroupCallParticipantsContext.Participant]
@ -84,6 +101,7 @@ final class VideoChatParticipantsComponent: Component {
let expandedVideoState: ExpandedVideoState?
let theme: PresentationTheme
let strings: PresentationStrings
let layoutType: LayoutType
let collapsedContainerInsets: UIEdgeInsets
let expandedContainerInsets: UIEdgeInsets
let sideInset: CGFloat
@ -97,6 +115,7 @@ final class VideoChatParticipantsComponent: Component {
expandedVideoState: ExpandedVideoState?,
theme: PresentationTheme,
strings: PresentationStrings,
layoutType: LayoutType,
collapsedContainerInsets: UIEdgeInsets,
expandedContainerInsets: UIEdgeInsets,
sideInset: CGFloat,
@ -109,6 +128,7 @@ final class VideoChatParticipantsComponent: Component {
self.expandedVideoState = expandedVideoState
self.theme = theme
self.strings = strings
self.layoutType = layoutType
self.collapsedContainerInsets = collapsedContainerInsets
self.expandedContainerInsets = expandedContainerInsets
self.sideInset = sideInset
@ -132,6 +152,9 @@ final class VideoChatParticipantsComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.layoutType != rhs.layoutType {
return false
}
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets {
return false
}
@ -214,15 +237,22 @@ final class VideoChatParticipantsComponent: Component {
struct ExpandedGrid {
let containerSize: CGSize
let layoutType: LayoutType
let containerInsets: UIEdgeInsets
init(containerSize: CGSize, containerInsets: UIEdgeInsets) {
init(containerSize: CGSize, layoutType: LayoutType, containerInsets: UIEdgeInsets) {
self.containerSize = containerSize
self.layoutType = layoutType
self.containerInsets = containerInsets
}
func itemContainerFrame() -> CGRect {
switch self.layoutType {
case .vertical:
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top - containerInsets.bottom))
case .horizontal:
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top))
}
}
}
@ -276,6 +306,7 @@ final class VideoChatParticipantsComponent: Component {
}
let containerSize: CGSize
let layoutType: LayoutType
let collapsedContainerInsets: UIEdgeInsets
let sideInset: CGFloat
let grid: Grid
@ -284,36 +315,93 @@ final class VideoChatParticipantsComponent: Component {
let spacing: CGFloat
let gridOffsetY: CGFloat
let listOffsetY: CGFloat
let listFrame: CGRect
let separateVideoGridFrame: CGRect
let scrollClippingFrame: CGRect
let separateVideoScrollClippingFrame: CGRect
init(containerSize: CGSize, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
init(containerSize: CGSize, layoutType: LayoutType, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
self.containerSize = containerSize
self.layoutType = layoutType
self.collapsedContainerInsets = collapsedContainerInsets
self.sideInset = sideInset
self.grid = Grid(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
self.expandedGrid = ExpandedGrid(containerSize: containerSize, containerInsets: expandedContainerInsets)
self.list = List(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
let gridWidth: CGFloat
let listWidth: CGFloat
switch layoutType {
case .vertical:
listWidth = containerSize.width - sideInset * 2.0
gridWidth = listWidth
case let .horizontal(horizontal):
listWidth = horizontal.rightColumnWidth
gridWidth = max(10.0, containerSize.width - sideInset * 2.0 - horizontal.rightColumnWidth - horizontal.columnSpacing)
}
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layoutType: layoutType, containerInsets: expandedContainerInsets)
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
self.spacing = 4.0
self.gridOffsetY = collapsedContainerInsets.top
var listOffsetY: CGFloat = self.gridOffsetY
if case .vertical = layoutType {
if self.grid.itemCount != 0 {
listOffsetY += self.grid.contentHeight()
listOffsetY += self.spacing
}
}
self.listOffsetY = listOffsetY
switch layoutType {
case .vertical:
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.sideInset, y: collapsedContainerInsets.top), size: CGSize(width: containerSize.width - self.sideInset * 2.0, height: containerSize.height - collapsedContainerInsets.top - collapsedContainerInsets.bottom))
self.listFrame = CGRect(origin: CGPoint(), size: containerSize)
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: -containerSize.width, y: 0.0), size: containerSize)
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
case let .horizontal(horizontal):
if horizontal.isCentered {
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - horizontal.rightColumnWidth) * 0.5), y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
} else {
self.listFrame = CGRect(origin: CGPoint(x: containerSize.width - self.sideInset - horizontal.rightColumnWidth, y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
}
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - collapsedContainerInsets.top))
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: min(self.sideInset, self.scrollClippingFrame.minX - horizontal.columnSpacing - gridWidth), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height))
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
}
}
func contentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY
switch self.layoutType {
case .vertical:
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
result += self.spacing
}
case .horizontal:
break
}
result += self.list.contentHeight()
result += self.collapsedContainerInsets.bottom
result += 32.0
result += 24.0
return result
}
func separateVideoGridContentHeight() -> CGFloat {
var result: CGFloat = self.gridOffsetY
switch self.layoutType {
case .vertical:
break
case .horizontal:
if self.grid.itemCount != 0 {
result += self.grid.contentHeight()
}
}
result += self.collapsedContainerInsets.bottom
result += 24.0
return result
}
@ -326,7 +414,12 @@ final class VideoChatParticipantsComponent: Component {
}
func gridItemContainerFrame() -> CGRect {
switch self.layoutType {
case .vertical:
return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight()))
case .horizontal:
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight()))
}
}
func visibleListItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
@ -338,7 +431,12 @@ final class VideoChatParticipantsComponent: Component {
}
func listItemContainerFrame() -> CGRect {
switch self.layoutType {
case .vertical:
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight()))
case .horizontal:
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.listFrame.width, height: self.list.contentHeight()))
}
}
func listTrailingItemFrame() -> CGRect {
@ -389,9 +487,13 @@ final class VideoChatParticipantsComponent: Component {
}
final class View: UIView, UIScrollViewDelegate {
private var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
private let scrollViewClippingContainer: SolidRoundedCornersContainer
private let scrollView: ScrollView
private let scrollViewClippingShadowView: UIImageView
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
private let separateVideoScrollView: ScrollView
private var component: VideoChatParticipantsComponent?
private var isUpdating: Bool = false
@ -422,10 +524,11 @@ final class VideoChatParticipantsComponent: Component {
override init(frame: CGRect) {
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
self.scrollViewClippingShadowView = UIImageView()
self.scrollView = ScrollView()
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
self.separateVideoScrollView = ScrollView()
self.gridItemViewContainer = UIView()
self.gridItemViewContainer.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
@ -453,13 +556,30 @@ final class VideoChatParticipantsComponent: Component {
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.scrollViewClippingShadowView)
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
self.addSubview(self.separateVideoScrollViewClippingContainer)
self.addSubview(self.separateVideoScrollViewClippingContainer.cornersView)
self.scrollView.addSubview(self.listItemViewContainer)
self.scrollView.addSubview(self.gridItemViewContainer)
self.addSubview(self.expandedGridItemContainer)
}
@ -481,6 +601,8 @@ final class VideoChatParticipantsComponent: Component {
} 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
}
@ -515,7 +637,10 @@ final class VideoChatParticipantsComponent: Component {
if component.expandedVideoState != nil {
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
} else {
switch itemLayout.layoutType {
case .vertical:
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
@ -523,6 +648,17 @@ final class VideoChatParticipantsComponent: Component {
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - component.collapsedContainerInsets.bottom {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - component.collapsedContainerInsets.bottom)
}
case .horizontal:
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: itemLayout.separateVideoScrollClippingFrame.minX, dy: 0.0).offsetBy(dx: 0.0, dy: -self.separateVideoScrollView.bounds.minY)
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
}
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height {
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height)
}
}
if expandedGridItemContainerFrame.size.height < 0.0 {
expandedGridItemContainerFrame.size.height = 0.0
}
@ -533,7 +669,13 @@ final class VideoChatParticipantsComponent: Component {
var validGridItemIds: [VideoParticipantKey] = []
var validGridItemIndices: [Int] = []
let visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
switch itemLayout.layoutType {
case .vertical:
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
case .horizontal:
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
}
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
let videoParticipant = self.gridParticipants[index]
@ -592,6 +734,16 @@ final class VideoChatParticipantsComponent: Component {
itemFrame = itemLayout.gridItemFrame(at: index)
}
var itemBottomInset: CGFloat = isItemExpanded ? 96.0 : 0.0
switch itemLayout.layoutType {
case .vertical:
break
case .horizontal:
if isItemExpanded {
itemBottomInset += itemLayout.expandedGrid.containerInsets.bottom
}
}
let _ = itemView.view.update(
transition: itemTransition,
component: AnyComponent(VideoChatParticipantVideoComponent(
@ -600,7 +752,8 @@ final class VideoChatParticipantsComponent: Component {
isPresentation: videoParticipant.isPresentation,
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
isExpanded: isItemExpanded,
bottomInset: isItemExpanded ? 96.0 : 0.0,
bottomInset: itemBottomInset,
rootVideoLoadingEffectView: self.rootVideoLoadingEffectView,
action: { [weak self] in
guard let self, let component = self.component else {
return
@ -893,7 +1046,13 @@ final class VideoChatParticipantsComponent: Component {
environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
)
let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
switch itemLayout.layoutType {
case .vertical:
break
case .horizontal:
expandedThumbnailsFrame.origin.y -= itemLayout.expandedGrid.containerInsets.bottom
}
if let expandedThumbnailsComponentView = expandedThumbnailsView.view {
if expandedThumbnailsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView)
@ -928,11 +1087,22 @@ final class VideoChatParticipantsComponent: Component {
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)
},
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: {},
@ -1011,6 +1181,28 @@ final class VideoChatParticipantsComponent: Component {
self.component = component
if !"".isEmpty {
let rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView
if let current = self.rootVideoLoadingEffectView {
rootVideoLoadingEffectView = current
} else {
rootVideoLoadingEffectView = VideoChatVideoLoadingEffectView(
effectAlpha: 0.1,
borderAlpha: 0.0,
gradientWidth: 260.0,
duration: 1.0,
hasCustomBorder: false,
playOnce: false
)
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
self.insertSubview(rootVideoLoadingEffectView, at: 0)
rootVideoLoadingEffectView.alpha = 0.0
rootVideoLoadingEffectView.isUserInteractionEnabled = false
}
rootVideoLoadingEffectView.update(size: availableSize, transition: transition)
}
let measureListItemSize = self.measureListItemView.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
@ -1080,6 +1272,7 @@ final class VideoChatParticipantsComponent: Component {
let itemLayout = ItemLayout(
containerSize: availableSize,
layoutType: component.layoutType,
sideInset: component.sideInset,
collapsedContainerInsets: component.collapsedContainerInsets,
expandedContainerInsets: component.expandedContainerInsets,
@ -1097,7 +1290,7 @@ final class VideoChatParticipantsComponent: Component {
cornerRadius: 10.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: itemLayout.list.contentHeight())
containerSize: CGSize(width: itemLayout.list.containerSize.width, height: itemLayout.list.contentHeight())
)
let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackgroundSize)
if let listItemsBackgroundView = self.listItemsBackground.view {
@ -1143,58 +1336,56 @@ final class VideoChatParticipantsComponent: Component {
}
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
let scrollClippingFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: component.collapsedContainerInsets.top), size: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: availableSize.height - component.collapsedContainerInsets.top - component.collapsedContainerInsets.bottom))
transition.setPosition(view: self.scrollViewClippingContainer, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
transition.setFrame(view: self.scrollViewClippingContainer.cornersView, frame: scrollClippingFrame)
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: scrollClippingFrame.size,
size: itemLayout.scrollClippingFrame.size,
color: .black,
cornerRadius: 10.0,
smoothCorners: false
), transition: transition)
if self.scrollViewClippingShadowView.image == nil {
let height: CGFloat = 24.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 - 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.scrollViewClippingShadowView.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.scrollViewClippingShadowView.tintColor = .black
}
let scrollViewClippingShadowHeight: CGFloat = 24.0
let scrollViewClippingShadowOffset: CGFloat = 0.0
transition.setFrame(view: self.scrollViewClippingShadowView, frame: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.maxY + scrollViewClippingShadowOffset - scrollViewClippingShadowHeight), size: CGSize(width: scrollClippingFrame.width, height: scrollViewClippingShadowHeight)))
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 != availableSize {
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
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: availableSize.width, height: itemLayout.contentHeight())
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
switch component.layoutType {
case .vertical:
if self.gridItemViewContainer.superview !== self.scrollView {
self.scrollView.addSubview(self.gridItemViewContainer)
}
case .horizontal:
if self.gridItemViewContainer.superview !== self.separateVideoScrollView {
self.separateVideoScrollView.addSubview(self.gridItemViewContainer)
}
}
self.updateScrolling(transition: transition)
return availableSize

View File

@ -0,0 +1,99 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import ComponentDisplayAdapters
final class VideoChatPinStatusComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let isPinned: Bool
let action: () -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
isPinned: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.isPinned = isPinned
self.action = action
}
static func ==(lhs: VideoChatPinStatusComponent, rhs: VideoChatPinStatusComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.isPinned != rhs.isPinned {
return false
}
return true
}
final class View: UIView {
private var pinNode: VoiceChatPinButtonNode?
private var component: VideoChatPinStatusComponent?
private var isUpdating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func pinPressed() {
guard let component = self.component else {
return
}
component.action()
}
func update(component: VideoChatPinStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
let pinNode: VoiceChatPinButtonNode
if let current = self.pinNode {
pinNode = current
} else {
pinNode = VoiceChatPinButtonNode(theme: component.theme, strings: component.strings)
self.pinNode = pinNode
self.addSubview(pinNode.view)
pinNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside)
}
let pinNodeSize = pinNode.update(size: availableSize, transition: transition.containedViewLayoutTransition)
let pinNodeFrame = CGRect(origin: CGPoint(), size: pinNodeSize)
transition.setFrame(view: pinNode.view, frame: pinNodeFrame)
pinNode.update(pinned: component.isPinned, animated: !transition.animation.isImmediate)
let size = pinNodeSize
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,130 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import HierarchyTrackingLayer
private let shadowImage: UIImage? = {
UIImage(named: "Stories/PanelGradient")
}()
final class VideoChatVideoLoadingEffectView: UIView {
private let duration: Double
private let hasCustomBorder: Bool
private let playOnce: Bool
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private let gradientWidth: CGFloat
let portalSource: PortalSourceView
private let backgroundView: UIImageView
private let borderGradientView: UIImageView
private let borderContainerView: UIView
let borderMaskLayer: SimpleShapeLayer
private var didPlayOnce = false
init(effectAlpha: CGFloat, borderAlpha: CGFloat, gradientWidth: CGFloat = 200.0, duration: Double, hasCustomBorder: Bool, playOnce: Bool) {
self.portalSource = PortalSourceView()
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
self.duration = duration
self.hasCustomBorder = hasCustomBorder
self.playOnce = playOnce
self.gradientWidth = gradientWidth
self.backgroundView = UIImageView()
self.borderGradientView = UIImageView()
self.borderContainerView = UIView()
self.borderMaskLayer = SimpleShapeLayer()
super.init(frame: .zero)
self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self, self.bounds.width != 0.0 else {
return
}
self.updateAnimations(size: self.bounds.size)
}
let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in
return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
if let shadowImage {
UIGraphicsPushContext(context)
for i in 0 ..< 2 {
let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))
context.saveGState()
context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY)
context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5)
let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width))
context.clip(to: adjustedRect, mask: shadowImage.cgImage!)
context.setFillColor(foregroundColor.cgColor)
context.fill(adjustedRect)
context.restoreGState()
}
UIGraphicsPopContext()
}
})
}
self.backgroundView.image = generateGradient(effectAlpha)
self.portalSource.addSubview(self.backgroundView)
self.borderGradientView.image = generateGradient(borderAlpha)
self.borderContainerView.addSubview(self.borderGradientView)
self.portalSource.addSubview(self.borderContainerView)
self.borderContainerView.layer.mask = self.borderMaskLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateAnimations(size: CGSize) {
if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) {
return
}
let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.2) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = self.playOnce ? 1 : Float.infinity
self.backgroundView.layer.add(animation, forKey: "shimmer")
self.borderGradientView.layer.add(animation, forKey: "shimmer")
self.didPlayOnce = true
}
func update(size: CGSize, transition: ComponentTransition) {
if self.backgroundView.bounds.size != size {
self.backgroundView.layer.removeAllAnimations()
if !self.hasCustomBorder {
self.borderMaskLayer.fillColor = nil
self.borderMaskLayer.strokeColor = UIColor.white.cgColor
let lineWidth: CGFloat = 3.0
self.borderMaskLayer.lineWidth = lineWidth
self.borderMaskLayer.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: 12.0).cgPath
}
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height)))
transition.setFrame(view: self.borderContainerView, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.borderGradientView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height)))
self.updateAnimations(size: size)
}
}

View File

@ -24,18 +24,18 @@ private let backgroundCornerRadius: CGFloat = 11.0
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30)
private class VoiceChatPinButtonNode: HighlightTrackingButtonNode {
class VoiceChatPinButtonNode: HighlightTrackingButtonNode {
private let pinButtonIconNode: VoiceChatPinNode
private let pinButtonClippingnode: ASDisplayNode
private let pinButtonTitleNode: ImmediateTextNode
init(presentationData: PresentationData) {
init(theme: PresentationTheme, strings: PresentationStrings) {
self.pinButtonIconNode = VoiceChatPinNode()
self.pinButtonClippingnode = ASDisplayNode()
self.pinButtonClippingnode.clipsToBounds = true
self.pinButtonTitleNode = ImmediateTextNode()
self.pinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white)
self.pinButtonTitleNode.attributedText = NSAttributedString(string: strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white)
self.pinButtonTitleNode.alpha = 0.0
super.init()
@ -209,7 +209,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.pinButtonNode = VoiceChatPinButtonNode(presentationData: presentationData)
self.pinButtonNode = VoiceChatPinButtonNode(theme: presentationData.theme, strings: presentationData.strings)
self.backdropAvatarNode = ImageNode()
self.backdropAvatarNode.contentMode = .scaleAspectFill