import Foundation import UIKit import Display import ComponentFlow import MultilineTextComponent import TelegramPresentationData import BundleIconComponent import MetalEngine import CallScreen import TelegramCore import AccountContext import SwiftSignalKit import DirectMediaImageCache import FastBlur import ContextUI import ComponentDisplayAdapters import AvatarNode private func blurredAvatarImage(_ dataImage: UIImage) -> UIImage? { let imageContextSize = CGSize(width: 64.0, height: 64.0) if let imageContext = DrawingContext(size: imageContextSize, scale: 1.0, clear: true) { imageContext.withFlippedContext { c in if let cgImage = dataImage.cgImage { c.draw(cgImage, in: CGRect(origin: CGPoint(), size: imageContextSize)) } } telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) return imageContext.generateImage() } else { return nil } } private let activityBorderImage: UIImage = { return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: .white, strokeWidth: 2.0)!.withRenderingMode(.alwaysTemplate) }() final class VideoChatParticipantVideoComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let call: VideoChatCall let participant: GroupCallParticipantsContext.Participant let isMyPeer: Bool let isPresentation: Bool let isSpeaking: Bool let maxVideoQuality: Int let isExpanded: Bool let isUIHidden: Bool let contentInsets: UIEdgeInsets let controlInsets: UIEdgeInsets let interfaceOrientation: UIInterfaceOrientation let enableVideoSharpening: Bool let action: (() -> Void)? let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? let activatePinch: ((PinchSourceContainerNode) -> Void)? let deactivatedPinch: (() -> Void)? init( theme: PresentationTheme, strings: PresentationStrings, call: VideoChatCall, participant: GroupCallParticipantsContext.Participant, isMyPeer: Bool, isPresentation: Bool, isSpeaking: Bool, maxVideoQuality: Int, isExpanded: Bool, isUIHidden: Bool, contentInsets: UIEdgeInsets, controlInsets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, enableVideoSharpening: Bool, action: (() -> Void)?, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?, activatePinch: ((PinchSourceContainerNode) -> Void)?, deactivatedPinch: (() -> Void)? ) { self.theme = theme self.strings = strings self.call = call self.participant = participant self.isMyPeer = isMyPeer self.isPresentation = isPresentation self.isSpeaking = isSpeaking self.maxVideoQuality = maxVideoQuality self.isExpanded = isExpanded self.isUIHidden = isUIHidden self.contentInsets = contentInsets self.controlInsets = controlInsets self.interfaceOrientation = interfaceOrientation self.enableVideoSharpening = enableVideoSharpening self.action = action self.contextAction = contextAction self.activatePinch = activatePinch self.deactivatedPinch = deactivatedPinch } static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool { if lhs.call != rhs.call { return false } if lhs.participant != rhs.participant { return false } if lhs.isMyPeer != rhs.isMyPeer { return false } if lhs.isPresentation != rhs.isPresentation { return false } if lhs.isSpeaking != rhs.isSpeaking { return false } if lhs.maxVideoQuality != rhs.maxVideoQuality { return false } if lhs.isExpanded != rhs.isExpanded { return false } if lhs.isUIHidden != rhs.isUIHidden { return false } if lhs.contentInsets != rhs.contentInsets { return false } if lhs.controlInsets != rhs.controlInsets { return false } if lhs.interfaceOrientation != rhs.interfaceOrientation { return false } if lhs.enableVideoSharpening != rhs.enableVideoSharpening { return false } if (lhs.action == nil) != (rhs.action == nil) { return false } if (lhs.contextAction == nil) != (rhs.contextAction == nil) { return false } if (lhs.activatePinch == nil) != (rhs.activatePinch == nil) { return false } if (lhs.deactivatedPinch == nil) != (rhs.deactivatedPinch == nil) { return false } return true } private struct VideoSpec: Equatable { var resolution: CGSize var rotationAngle: Float var followsDeviceOrientation: Bool init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool) { self.resolution = resolution self.rotationAngle = rotationAngle self.followsDeviceOrientation = followsDeviceOrientation } } private struct ReferenceLocation: Equatable { var containerWidth: CGFloat var positionX: CGFloat init(containerWidth: CGFloat, positionX: CGFloat) { self.containerWidth = containerWidth self.positionX = positionX } } private final class AnimationHint { enum Kind { case videoAvailabilityChanged } let kind: Kind init(kind: Kind) { self.kind = kind } } final class View: ContextControllerSourceView { private var component: VideoChatParticipantVideoComponent? private weak var componentState: EmptyComponentState? private var isUpdating: Bool = false private var previousSize: CGSize? private let backgroundGradientView: UIImageView private let muteStatus = ComponentView() private let title = ComponentView() private var blurredAvatarDisposable: Disposable? private var blurredAvatarView: UIImageView? private let pinchContainerNode: PinchSourceContainerNode private let extractedContainerView: ContextExtractedContentContainingView private var videoSource: AdaptedCallVideoSource? private var videoPlaceholder: VideoSource.Output? private var videoDisposable: Disposable? private var videoBackgroundLayer: SimpleLayer? private var videoLayer: PrivateCallVideoLayer? private var videoSpec: VideoSpec? private var awaitingFirstVideoFrameForUnpause: Bool = false private var videoStatus: ComponentView? private var activityBorderView: UIImageView? private var referenceLocation: ReferenceLocation? private var loadingEffectView: VideoChatVideoLoadingEffectView? override init(frame: CGRect) { self.backgroundGradientView = UIImageView() self.pinchContainerNode = PinchSourceContainerNode() self.extractedContainerView = ContextExtractedContentContainingView() super.init(frame: frame) self.addSubview(self.extractedContainerView) self.targetViewForActivationProgress = self.extractedContainerView.contentView self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view) self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView) //TODO:release optimize self.pinchContainerNode.contentNode.view.layer.cornerRadius = 10.0 self.pinchContainerNode.contentNode.view.clipsToBounds = true self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.pinchContainerNode.activate = { [weak self] sourceNode in guard let self, let component = self.component else { return } component.activatePinch?(sourceNode) } self.pinchContainerNode.animatedOut = { [weak self] in guard let self, let component = self.component else { return } component.deactivatedPinch?() } self.activated = { [weak self] gesture, _ in guard let self, let component = self.component else { gesture.cancel() return } if let participantPeer = component.participant.peer { component.contextAction?(participantPeer, self.extractedContainerView, gesture) } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.videoDisposable?.dispose() self.blurredAvatarDisposable?.dispose() } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { guard let component = self.component, let action = component.action else { return } action() } } func updatePlaceholder(placeholder: VideoSource.Output) { self.videoPlaceholder = placeholder self.componentState?.updated(transition: .immediate, isLocal: true) } func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let previousComponent = self.component self.component = component self.componentState = state self.isGestureEnabled = !component.isExpanded self.pinchContainerNode.isPinchGestureEnabled = component.activatePinch != nil transition.setPosition(view: self.pinchContainerNode.view, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setBounds(view: self.pinchContainerNode.view, bounds: CGRect(origin: CGPoint(), size: availableSize)) self.pinchContainerNode.update(size: availableSize, transition: transition.containedViewLayoutTransition) transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize)) self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.backgroundGradientView, frame: CGRect(origin: CGPoint(), size: availableSize)) let alphaTransition: ComponentTransition if !transition.animation.isImmediate { alphaTransition = .easeInOut(duration: 0.2) } else { alphaTransition = .immediate } let videoAlphaTransition: ComponentTransition if let animationHint = transition.userData(AnimationHint.self), case .videoAvailabilityChanged = animationHint.kind { videoAlphaTransition = .easeInOut(duration: 0.2) } else { videoAlphaTransition = alphaTransition } let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0 if previousComponent == nil { let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer?.id, nameColor: component.participant.peer?.nameColor, icon: .none, theme: component.theme) self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: colors.reversed(), locations: [0.0, 1.0], direction: .vertical) } if let smallProfileImage = component.participant.peer?.smallProfileImage { let blurredAvatarView: UIImageView if let current = self.blurredAvatarView { blurredAvatarView = current transition.setFrame(view: blurredAvatarView, frame: CGRect(origin: CGPoint(), size: availableSize)) } else { blurredAvatarView = UIImageView() blurredAvatarView.contentMode = .scaleAspectFill self.blurredAvatarView = blurredAvatarView self.pinchContainerNode.contentNode.view.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView) blurredAvatarView.frame = CGRect(origin: CGPoint(), size: availableSize) } if self.blurredAvatarDisposable == nil { //TODO:release synchronous if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer._asPeer()) { if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: participantPeer.profileImageRepresentations.first?.immediateThumbnailData, size: 64, synchronous: false) { if let image = result.image { blurredAvatarView.image = blurredAvatarImage(image) } if let loadSignal = result.loadSignal { self.blurredAvatarDisposable = (loadSignal |> deliverOnMainQueue).startStrict(next: { [weak self] image in guard let self else { return } if let image { self.blurredAvatarView?.image = blurredAvatarImage(image) } else { self.blurredAvatarView?.image = nil } }) } } } } } else { if let blurredAvatarView = self.blurredAvatarView { self.blurredAvatarView = nil blurredAvatarView.removeFromSuperview() } if let blurredAvatarDisposable = self.blurredAvatarDisposable { self.blurredAvatarDisposable = nil blurredAvatarDisposable.dispose() } } let muteStatusSize = self.muteStatus.update( transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking), shadowColor: UIColor(white: 0.0, alpha: 0.7), shadowBlur: 8.0 )), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) ) let muteStatusFrame: CGRect if component.isExpanded { muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.controlInsets.bottom + 1.0 - muteStatusSize.height), size: muteStatusSize) } else { muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.controlInsets.bottom + 3.0 - muteStatusSize.height), size: muteStatusSize) } if let muteStatusView = self.muteStatus.view { if muteStatusView.superview == nil { self.pinchContainerNode.contentNode.view.addSubview(muteStatusView) muteStatusView.alpha = controlsAlpha } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7) alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha) } let titleInnerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.participant.peer?.debugDisplayTitle ?? "User \(component.participant.id)", font: Font.semibold(16.0), textColor: .white)), insets: titleInnerInsets, textShadowColor: UIColor(white: 0.0, alpha: 0.7), textShadowBlur: 8.0 )), environment: {}, containerSize: CGSize(width: availableSize.width - 8.0 * 2.0 - 4.0, height: 100.0) ) let titleFrame: CGRect if component.isExpanded { titleFrame = CGRect(origin: CGPoint(x: 36.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height + titleInnerInsets.top), size: titleSize) } else { titleFrame = CGRect(origin: CGPoint(x: 29.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height + titleInnerInsets.top + 1.0), size: titleSize) } if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() self.pinchContainerNode.contentNode.view.addSubview(titleView) titleView.alpha = controlsAlpha } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825) alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } var previousVideoDescription: GroupCallParticipantsContext.Participant.VideoDescription? if let previousComponent { if previousComponent.isMyPeer && previousComponent.isPresentation { previousVideoDescription = nil } else { previousVideoDescription = previousComponent.maxVideoQuality == 0 ? nil : (previousComponent.isPresentation ? previousComponent.participant.presentationDescription : previousComponent.participant.videoDescription) } } let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? if component.isMyPeer && component.isPresentation { videoDescription = nil } else { videoDescription = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription) } var isEffectivelyPaused = false if let videoDescription, videoDescription.isPaused { isEffectivelyPaused = true } else if let previousComponent { let previousVideoDescription = previousComponent.isPresentation ? previousComponent.participant.presentationDescription : previousComponent.participant.videoDescription if let previousVideoDescription, previousVideoDescription.isPaused { self.awaitingFirstVideoFrameForUnpause = true } if self.awaitingFirstVideoFrameForUnpause { isEffectivelyPaused = true } } if let videoDescription { let videoBackgroundLayer: SimpleLayer if let current = self.videoBackgroundLayer { videoBackgroundLayer = current } else { videoBackgroundLayer = SimpleLayer() videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor videoBackgroundLayer.opacity = 0.0 self.videoBackgroundLayer = videoBackgroundLayer if let blurredAvatarView = self.blurredAvatarView { self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer) } else { self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer) } videoBackgroundLayer.isHidden = true } let videoUpdated: () -> Void = { [weak self] in guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else { return } var videoOutput = videoSource.currentOutput var isPlaceholder = false if videoOutput == nil { isPlaceholder = true videoOutput = self.videoPlaceholder } else { self.videoPlaceholder = nil } videoLayer.video = videoOutput if let videoOutput { let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { self.awaitingFirstVideoFrameForUnpause = false self.videoSpec = videoSpec if !self.isUpdating { var transition: ComponentTransition = .immediate if !isPlaceholder { transition = transition.withUserData(AnimationHint(kind: .videoAvailabilityChanged)) } self.componentState?.updated(transition: transition, isLocal: true) } } } else { if self.videoSpec != nil { self.videoSpec = nil if !self.isUpdating { self.componentState?.updated(transition: .immediate, isLocal: true) } } } } let videoLayer: PrivateCallVideoLayer var resetVideoSource = false if let current = self.videoLayer { videoLayer = current if let previousVideoDescription, previousVideoDescription.endpointId != videoDescription.endpointId { resetVideoSource = true } } else { videoLayer = PrivateCallVideoLayer(enableSharpening: component.enableVideoSharpening) self.videoLayer = videoLayer videoLayer.opacity = 0.0 self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) videoLayer.blurredLayer.opacity = 0.0 resetVideoSource = true } if resetVideoSource { if let input = component.call.video(endpointId: videoDescription.endpointId) { let videoSource = AdaptedCallVideoSource(videoStreamSignal: input) self.videoSource = videoSource self.videoDisposable?.dispose() self.videoDisposable = videoSource.addOnUpdated { videoUpdated() } } } if let _ = self.videoPlaceholder, videoLayer.video == nil { videoUpdated() } transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) if let videoSpec = self.videoSpec { if videoBackgroundLayer.isHidden { videoBackgroundLayer.isHidden = false } videoAlphaTransition.setAlpha(layer: videoBackgroundLayer, alpha: 1.0) if isEffectivelyPaused { videoAlphaTransition.setAlpha(layer: videoLayer, alpha: 0.0) videoAlphaTransition.setAlpha(layer: videoLayer.blurredLayer, alpha: 0.9) } else { videoAlphaTransition.setAlpha(layer: videoLayer, alpha: 1.0) videoAlphaTransition.setAlpha(layer: videoLayer.blurredLayer, alpha: 0.25) } let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation) var rotatedResolution = videoSpec.resolution var videoIsRotated = false if abs(rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { videoIsRotated = true } if videoIsRotated { rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) } let videoSize = rotatedResolution.aspectFitted(availableSize) let videoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - videoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - videoSize.height) * 0.5)), size: videoSize) let blurredVideoSize = rotatedResolution.aspectFilled(availableSize) let blurredVideoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - blurredVideoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - blurredVideoSize.height) * 0.5)), size: blurredVideoSize) let videoResolution = rotatedResolution var rotatedVideoResolution = videoResolution var rotatedVideoFrame = videoFrame var rotatedBlurredVideoFrame = blurredVideoFrame var rotatedVideoBoundsSize = videoFrame.size var rotatedBlurredVideoBoundsSize = blurredVideoFrame.size if videoIsRotated { rotatedVideoBoundsSize = CGSize(width: rotatedVideoBoundsSize.height, height: rotatedVideoBoundsSize.width) rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center) rotatedBlurredVideoBoundsSize = CGSize(width: rotatedBlurredVideoBoundsSize.height, height: rotatedBlurredVideoBoundsSize.width) rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center) } rotatedVideoResolution = rotatedVideoResolution.aspectFittedOrSmaller(CGSize(width: rotatedVideoFrame.width * UIScreenScale, height: rotatedVideoFrame.height * UIScreenScale)) transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize)) transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center) transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoBoundsSize)) transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) } } else { if let videoBackgroundLayer = self.videoBackgroundLayer { self.videoBackgroundLayer = nil videoBackgroundLayer.removeFromSuperlayer() } if let videoLayer = self.videoLayer { self.videoLayer = nil videoLayer.blurredLayer.removeFromSuperlayer() videoLayer.removeFromSuperlayer() } self.videoDisposable?.dispose() self.videoDisposable = nil self.videoSource = nil self.videoSpec = nil } var statusKind: VideoChatParticipantVideoStatusComponent.Kind? if component.isPresentation && component.isMyPeer { statusKind = .ownScreenshare } else if isEffectivelyPaused { statusKind = .paused } if let statusKind { let videoStatus: ComponentView var videoStatusTransition = transition if let current = self.videoStatus { videoStatus = current } else { videoStatusTransition = videoStatusTransition.withAnimation(.none) videoStatus = ComponentView() self.videoStatus = videoStatus } let _ = videoStatus.update( transition: videoStatusTransition, component: AnyComponent(VideoChatParticipantVideoStatusComponent( strings: component.strings, kind: statusKind, isExpanded: component.isExpanded )), environment: {}, containerSize: availableSize ) if let videoStatusView = videoStatus.view { if videoStatusView.superview == nil { videoStatusView.isUserInteractionEnabled = false videoStatusView.alpha = 0.0 self.pinchContainerNode.contentNode.view.addSubview(videoStatusView) } videoStatusTransition.setFrame(view: videoStatusView, frame: CGRect(origin: CGPoint(), size: availableSize)) videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 1.0) } } else if let videoStatus = self.videoStatus { self.videoStatus = nil if let videoStatusView = videoStatus.view { videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 0.0, completion: { [weak videoStatusView] _ in videoStatusView?.removeFromSuperview() }) } } if videoDescription != nil && self.videoSpec == nil && !isEffectivelyPaused { if self.loadingEffectView == nil { let loadingEffectView = VideoChatVideoLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, cornerRadius: 10.0, duration: 1.0) self.loadingEffectView = loadingEffectView loadingEffectView.alpha = 0.0 loadingEffectView.isUserInteractionEnabled = false self.pinchContainerNode.contentNode.view.addSubview(loadingEffectView) if let referenceLocation = self.referenceLocation { self.updateHorizontalReferenceLocation(containerWidth: referenceLocation.containerWidth, positionX: referenceLocation.positionX, transition: .immediate) } videoAlphaTransition.setAlpha(view: loadingEffectView, alpha: 1.0) } } else if let loadingEffectView = self.loadingEffectView { self.loadingEffectView = nil videoAlphaTransition.setAlpha(view: loadingEffectView, alpha: 0.0, completion: { [weak loadingEffectView] _ in loadingEffectView?.removeFromSuperview() }) } if component.isSpeaking && !component.isExpanded { let activityBorderView: UIImageView if let current = self.activityBorderView { activityBorderView = current } else { activityBorderView = UIImageView() self.activityBorderView = activityBorderView self.pinchContainerNode.contentNode.view.addSubview(activityBorderView) activityBorderView.image = activityBorderImage activityBorderView.tintColor = UIColor(rgb: 0x33C758) if let previousSize { activityBorderView.frame = CGRect(origin: CGPoint(), size: previousSize) } } } else if let activityBorderView = self.activityBorderView { if !transition.animation.isImmediate { let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) if activityBorderView.alpha != 0.0 { alphaTransition.setAlpha(view: activityBorderView, alpha: 0.0, completion: { [weak self, weak activityBorderView] completed in guard let self, let component = self.component, let activityBorderView, self.activityBorderView === activityBorderView, completed else { return } if !component.isSpeaking { activityBorderView.removeFromSuperview() self.activityBorderView = nil } }) } } else { self.activityBorderView = nil activityBorderView.removeFromSuperview() } } if let activityBorderView = self.activityBorderView { transition.setFrame(view: activityBorderView, frame: CGRect(origin: CGPoint(), size: availableSize)) } self.previousSize = availableSize return availableSize } func updateHorizontalReferenceLocation(containerWidth: CGFloat, positionX: CGFloat, transition: ComponentTransition) { self.referenceLocation = ReferenceLocation(containerWidth: containerWidth, positionX: positionX) if let loadingEffectView = self.loadingEffectView, let size = self.previousSize { transition.setFrame(view: loadingEffectView, frame: CGRect(origin: CGPoint(), size: size)) loadingEffectView.update(size: size, containerWidth: containerWidth, offsetX: positionX, gradientWidth: floor(containerWidth * 0.8), transition: transition) } } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }