diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift index 716bfd8076..c75453e7cf 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift @@ -19,6 +19,7 @@ final class VideoChatParticipantThumbnailComponent: Component { let isPresentation: Bool let isSelected: Bool let isSpeaking: Bool + let interfaceOrientation: UIInterfaceOrientation let action: (() -> Void)? init( @@ -28,6 +29,7 @@ final class VideoChatParticipantThumbnailComponent: Component { isPresentation: Bool, isSelected: Bool, isSpeaking: Bool, + interfaceOrientation: UIInterfaceOrientation, action: (() -> Void)? ) { self.call = call @@ -36,6 +38,7 @@ final class VideoChatParticipantThumbnailComponent: Component { self.isPresentation = isPresentation self.isSelected = isSelected self.isSpeaking = isSpeaking + self.interfaceOrientation = interfaceOrientation self.action = action } @@ -58,16 +61,21 @@ final class VideoChatParticipantThumbnailComponent: Component { if lhs.isSpeaking != rhs.isSpeaking { return false } + if lhs.interfaceOrientation != rhs.interfaceOrientation { + return false + } return true } private struct VideoSpec: Equatable { var resolution: CGSize var rotationAngle: Float + var followsDeviceOrientation: Bool - init(resolution: CGSize, rotationAngle: Float) { + init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool) { self.resolution = resolution self.rotationAngle = rotationAngle + self.followsDeviceOrientation = followsDeviceOrientation } } @@ -243,7 +251,7 @@ final class VideoChatParticipantThumbnailComponent: Component { videoLayer.video = videoOutput if let videoOutput { - let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle) + let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) if self.videoSpec != videoSpec { self.videoSpec = videoSpec if !self.isUpdating { @@ -269,9 +277,11 @@ final class VideoChatParticipantThumbnailComponent: Component { videoLayer.blurredLayer.isHidden = component.isSelected videoLayer.isHidden = component.isSelected + let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation) + var rotatedResolution = videoSpec.resolution var videoIsRotated = false - if abs(videoSpec.rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(videoSpec.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { + if abs(rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { videoIsRotated = true } if videoIsRotated { @@ -303,12 +313,12 @@ final class VideoChatParticipantThumbnailComponent: Component { transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize)) - transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) + 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(videoSpec.rotationAngle), 0.0, 0.0, 1.0)) + transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) } } else { if let videoBackgroundLayer = self.videoBackgroundLayer { @@ -426,6 +436,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { let participants: [Participant] let selectedParticipant: Participant.Key? let speakingParticipants: Set + let interfaceOrientation: UIInterfaceOrientation let updateSelectedParticipant: (Participant.Key) -> Void init( @@ -434,6 +445,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { participants: [Participant], selectedParticipant: Participant.Key?, speakingParticipants: Set, + interfaceOrientation: UIInterfaceOrientation, updateSelectedParticipant: @escaping (Participant.Key) -> Void ) { self.call = call @@ -441,6 +453,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { self.participants = participants self.selectedParticipant = selectedParticipant self.speakingParticipants = speakingParticipants + self.interfaceOrientation = interfaceOrientation self.updateSelectedParticipant = updateSelectedParticipant } @@ -460,6 +473,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { if lhs.speakingParticipants != rhs.speakingParticipants { return false } + if lhs.interfaceOrientation != rhs.interfaceOrientation { + return false + } return true } @@ -595,6 +611,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { isPresentation: participant.isPresentation, isSelected: component.selectedParticipant == participant.key, isSpeaking: component.speakingParticipants.contains(participant.participant.peer.id), + interfaceOrientation: component.interfaceOrientation, action: { [weak self] in guard let self, let component = self.component else { return diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index bc631b0f84..53d5df260f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -97,20 +97,17 @@ private final class BlobView: UIView { } private func updateAudioLevel() { - let additionalAvatarScale = CGFloat(max(0.0, min(self.presentationAudioLevel * 18.0, 5.0)) * 0.05) - let blobAmplificationFactor: CGFloat = 2.0 - let blobScale = 1.0 + additionalAvatarScale * blobAmplificationFactor + let additionalAvatarScale = CGFloat(max(0.0, min(self.presentationAudioLevel * 0.3, 1.0)) * 1.0) + let blobScale = 1.28 + additionalAvatarScale self.blobsLayer.transform = CATransform3DMakeScale(blobScale, blobScale, 1.0) - self.scaleUpdated?(blobScale) + self.scaleUpdated?(additionalAvatarScale) } public func startAnimating() { guard !self.isAnimating else { return } self.isAnimating = true - self.updateBlobsState() - self.displayLinkAnimator?.isPaused = false } @@ -122,34 +119,15 @@ private final class BlobView: UIView { guard isAnimating else { return } self.isAnimating = false - self.updateBlobsState() - self.displayLinkAnimator?.isPaused = true } - private func updateBlobsState() { - /*if self.isAnimating { - if self.mediumBlob.frame.size != .zero { - self.mediumBlob.startAnimating() - self.bigBlob.startAnimating() - } - } else { - self.mediumBlob.stopAnimating() - self.bigBlob.stopAnimating() - }*/ - } - - override public func layoutSubviews() { + func update(size: CGSize) { super.layoutSubviews() - //self.mediumBlob.frame = bounds - //self.bigBlob.frame = bounds - - let blobsFrame = bounds.insetBy(dx: floor(bounds.width * 0.12), dy: floor(bounds.height * 0.12)) + let blobsFrame = CGRect(origin: CGPoint(), size: size) self.blobsLayer.position = blobsFrame.center self.blobsLayer.bounds = CGRect(origin: CGPoint(), size: blobsFrame.size) - - self.updateBlobsState() } } @@ -268,9 +246,13 @@ final class VideoChatParticipantAvatarComponent: Component { avatarNode.setPeer(context: component.call.accountContext, theme: component.theme, peer: component.peer, clipStyle: clipStyle, synchronousLoad: false, displayDimensions: avatarSize) } - transition.setFrame(view: avatarNode.view, frame: CGRect(origin: CGPoint(), size: avatarSize)) + let avatarFrame = CGRect(origin: CGPoint(), size: avatarSize) + transition.setPosition(view: avatarNode.view, position: avatarFrame.center) + transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) avatarNode.updateSize(size: avatarSize) + let blobScale: CGFloat = 1.5 + if self.audioLevelDisposable == nil { let peerId = component.peer.id struct Level { @@ -314,10 +296,22 @@ final class VideoChatParticipantAvatarComponent: Component { bigBlobRange: (0.71, 1.0) ) self.blobView = blobView - blobView.frame = avatarNode.frame + let blobSize = floor(avatarNode.bounds.width * blobScale) + blobView.center = avatarNode.frame.center + blobView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: blobSize, height: blobSize)) + blobView.layer.transform = CATransform3DMakeScale(1.0 / blobScale, 1.0 / blobScale, 1.0) + + blobView.update(size: blobView.bounds.size) self.insertSubview(blobView, belowSubview: avatarNode.view) - blobView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + blobView.layer.animateScale(from: 0.5, to: 1.0 / blobScale, duration: 0.2) + + blobView.scaleUpdated = { [weak self] additionalScale in + guard let self, let avatarNode = self.avatarNode else { + return + } + avatarNode.layer.transform = CATransform3DMakeScale(1.0 + additionalScale, 1.0 + additionalScale, 1.0) + } ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) } @@ -342,7 +336,11 @@ final class VideoChatParticipantAvatarComponent: Component { blobView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak blobView] _ in blobView?.removeFromSuperview() }) - blobView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) + blobView.layer.animateScale(from: 1.0 / blobScale, to: 0.5, duration: 0.3, removeOnCompletion: false) + let transition: ComponentTransition = .easeInOut(duration: 0.1) + if let avatarNode = self.avatarNode { + transition.setScale(view: avatarNode.view, scale: 1.0) + } } }) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index cde0ff9ad9..836747c627 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -35,8 +35,10 @@ private let activityBorderImage: UIImage = { }() final class VideoChatParticipantVideoComponent: Component { + let strings: PresentationStrings let call: PresentationGroupCall let participant: GroupCallParticipantsContext.Participant + let isMyPeer: Bool let isPresentation: Bool let isSpeaking: Bool let isExpanded: Bool @@ -44,12 +46,13 @@ final class VideoChatParticipantVideoComponent: Component { let contentInsets: UIEdgeInsets let controlInsets: UIEdgeInsets let interfaceOrientation: UIInterfaceOrientation - weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? let action: (() -> Void)? init( + strings: PresentationStrings, call: PresentationGroupCall, participant: GroupCallParticipantsContext.Participant, + isMyPeer: Bool, isPresentation: Bool, isSpeaking: Bool, isExpanded: Bool, @@ -57,11 +60,12 @@ final class VideoChatParticipantVideoComponent: Component { contentInsets: UIEdgeInsets, controlInsets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, - rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?, action: (() -> Void)? ) { + self.strings = strings self.call = call self.participant = participant + self.isMyPeer = isMyPeer self.isPresentation = isPresentation self.isSpeaking = isSpeaking self.isExpanded = isExpanded @@ -69,7 +73,6 @@ final class VideoChatParticipantVideoComponent: Component { self.contentInsets = contentInsets self.controlInsets = controlInsets self.interfaceOrientation = interfaceOrientation - self.rootVideoLoadingEffectView = rootVideoLoadingEffectView self.action = action } @@ -77,6 +80,9 @@ final class VideoChatParticipantVideoComponent: Component { if lhs.participant != rhs.participant { return false } + if lhs.isMyPeer != rhs.isMyPeer { + return false + } if lhs.isPresentation != rhs.isPresentation { return false } @@ -116,12 +122,36 @@ final class VideoChatParticipantVideoComponent: Component { } } + 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: HighlightTrackingButton { 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() @@ -134,13 +164,20 @@ final class VideoChatParticipantVideoComponent: Component { private var videoLayer: PrivateCallVideoLayer? private var videoSpec: VideoSpec? + private var awaitingFirstVideoFrameForUnpause: Bool = false + private var videoStatus: ComponentView? private var activityBorderView: UIImageView? - private var loadingEffectView: PortalView? + private var referenceLocation: ReferenceLocation? + private var loadingEffectView: VideoChatVideoLoadingEffectView? override init(frame: CGRect) { + self.backgroundGradientView = UIImageView() + super.init(frame: frame) + self.addSubview(self.backgroundGradientView) + //TODO:release optimize self.clipsToBounds = true self.layer.cornerRadius = 10.0 @@ -170,9 +207,12 @@ final class VideoChatParticipantVideoComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.componentState = state + transition.setFrame(view: self.backgroundGradientView, frame: CGRect(origin: CGPoint(), size: availableSize)) + let alphaTransition: ComponentTransition if !transition.animation.isImmediate { alphaTransition = .easeInOut(duration: 0.2) @@ -180,11 +220,24 @@ final class VideoChatParticipantVideoComponent: Component { 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 let nameColor = component.participant.peer.nameColor ?? .blue let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true) - self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4) + + if previousComponent == nil { + self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: [ + nameColors.main.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 1.3), + nameColors.main.withMultiplied(hue: 1.0, saturation: 1.2, brightness: 1.0) + ], locations: [0.0, 1.0], direction: .vertical) + } if let smallProfileImage = component.participant.peer.smallProfileImage { let blurredAvatarView: UIImageView @@ -196,7 +249,7 @@ final class VideoChatParticipantVideoComponent: Component { blurredAvatarView = UIImageView() blurredAvatarView.contentMode = .scaleAspectFill self.blurredAvatarView = blurredAvatarView - self.insertSubview(blurredAvatarView, at: 0) + self.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView) blurredAvatarView.frame = CGRect(origin: CGPoint(), size: availableSize) } @@ -287,18 +340,34 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } - if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { + let videoDescription = 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.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer) } else { - self.layer.insertSublayer(videoBackgroundLayer, at: 0) + self.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer) } videoBackgroundLayer.isHidden = true } @@ -309,10 +378,11 @@ final class VideoChatParticipantVideoComponent: Component { } else { videoLayer = PrivateCallVideoLayer() self.videoLayer = videoLayer + videoLayer.opacity = 0.0 self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) - videoLayer.blurredLayer.opacity = 0.25 + videoLayer.blurredLayer.opacity = 0.0 if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) { let videoSource = AdaptedCallVideoSource(videoStreamSignal: input) @@ -329,10 +399,12 @@ final class VideoChatParticipantVideoComponent: Component { if let videoOutput { let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) - if self.videoSpec != videoSpec { + if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause { + self.awaitingFirstVideoFrameForUnpause = false + self.videoSpec = videoSpec if !self.isUpdating { - self.componentState?.updated(transition: .immediate, isLocal: true) + self.componentState?.updated(transition: ComponentTransition.immediate.withUserData(AnimationHint(kind: .videoAvailabilityChanged)), isLocal: true) } } } else { @@ -350,7 +422,19 @@ final class VideoChatParticipantVideoComponent: Component { transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) if let videoSpec = self.videoSpec { - videoBackgroundLayer.isHidden = false + 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) @@ -410,17 +494,69 @@ 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.isUserInteractionEnabled = false - loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize) + 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.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 let loadingEffectView = self.loadingEffectView { - transition.setFrame(view: loadingEffectView.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + + 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.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 { @@ -467,6 +603,15 @@ final class VideoChatParticipantVideoComponent: Component { 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 { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoStatusComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoStatusComponent.swift new file mode 100644 index 0000000000..f8f91cc9ee --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoStatusComponent.swift @@ -0,0 +1,140 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import BundleIconComponent +import MultilineTextComponent + +final class VideoChatParticipantVideoStatusComponent: Component { + enum Kind { + case ownScreenshare + case paused + } + + let strings: PresentationStrings + let kind: Kind + let isExpanded: Bool + + init( + strings: PresentationStrings, + kind: Kind, + isExpanded: Bool + ) { + self.strings = strings + self.kind = kind + self.isExpanded = isExpanded + } + + static func ==(lhs: VideoChatParticipantVideoStatusComponent, rhs: VideoChatParticipantVideoStatusComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.kind != rhs.kind { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private var icon = ComponentView() + private let title = ComponentView() + + private var component: VideoChatParticipantVideoStatusComponent? + 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 { + } + + func update(component: VideoChatParticipantVideoStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousComponent = self.component + self.component = component + + var iconTransition = transition + if let previousComponent, previousComponent.kind != component.kind { + self.icon.view?.removeFromSuperview() + self.icon = ComponentView() + iconTransition = iconTransition.withAnimation(.none) + } + + let iconName: String + let titleValue: String + switch component.kind { + case .ownScreenshare: + iconName = "Call/ScreenSharePhone" + titleValue = component.strings.VoiceChat_YouAreSharingScreen + case .paused: + iconName = "Call/Pause" + titleValue = component.strings.VoiceChat_VideoPaused + } + + let iconSize = self.icon.update( + transition: iconTransition, + component: AnyComponent(BundleIconComponent( + name: iconName, + tintColor: .white + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleValue, font: Font.semibold(14.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0) + ) + + let scale: CGFloat = component.isExpanded ? 1.0 : 0.825 + + let spacing: CGFloat = 18.0 + let contentHeight: CGFloat = iconSize.height + spacing + titleSize.height + + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconTransition.setFrame(view: iconView, frame: iconFrame) + } + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + iconTransition.setFrame(view: titleView, frame: titleFrame) + } + + iconTransition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0)) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + 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) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index f4fe335ade..2174d6f161 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -591,8 +591,6 @@ final class VideoChatParticipantsComponent: Component { } final class View: UIView, UIScrollViewDelegate { - private var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? - private let scrollViewClippingContainer: SolidRoundedCornersContainer private let scrollView: ScrollView @@ -628,6 +626,8 @@ final class VideoChatParticipantsComponent: Component { private var appliedGridIsEmpty: Bool = true + private var currentLoadMoreToken: String? + override init(frame: CGRect) { self.scrollViewClippingContainer = SolidRoundedCornersContainer() self.scrollView = ScrollView() @@ -885,6 +885,14 @@ final class VideoChatParticipantsComponent: Component { 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() @@ -912,8 +920,10 @@ final class VideoChatParticipantsComponent: Component { let _ = itemView.view.update( transition: itemTransition, component: AnyComponent(VideoChatParticipantVideoComponent( + strings: component.strings, call: component.call, participant: videoParticipant.participant, + isMyPeer: videoParticipant.participant.peer.id == component.participants?.myPeerId, isPresentation: videoParticipant.isPresentation, isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), isExpanded: isItemExpanded, @@ -921,7 +931,6 @@ final class VideoChatParticipantsComponent: Component { contentInsets: itemContentInsets, controlInsets: itemControlInsets, interfaceOrientation: component.interfaceOrientation, - rootVideoLoadingEffectView: self.rootVideoLoadingEffectView, action: { [weak self] in guard let self, let component = self.component else { return @@ -936,7 +945,7 @@ final class VideoChatParticipantsComponent: Component { environment: {}, containerSize: itemFrame.size ) - if let itemComponentView = itemView.view.view { + if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View { if itemComponentView.superview == nil { itemComponentView.layer.allowsGroupOpacity = true @@ -952,6 +961,7 @@ final class VideoChatParticipantsComponent: Component { 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) @@ -986,11 +996,13 @@ final class VideoChatParticipantsComponent: Component { 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 { @@ -1237,6 +1249,7 @@ final class VideoChatParticipantsComponent: Component { 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 @@ -1371,6 +1384,13 @@ final class VideoChatParticipantsComponent: Component { } } } + + 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) + } + } } func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index da973d873d..306499852f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1979,6 +1979,96 @@ private final class VideoChatScreenComponent: Component { } } + private func onLeavePressed() { + guard let component = self.component, let environment = self.environment else { + return + } + + //TODO:release + let isScheduled = !"".isEmpty + + let action: (Bool) -> Void = { [weak self] terminateIfPossible in + guard let self, let component = self.component else { + return + } + + let _ = component.call.leave(terminateIfPossible: terminateIfPossible).startStandalone() + + if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { + controller.dismiss(closing: true, manual: false) + } + } + + if let callState = self.callState, callState.canManageCall { + let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + let leaveTitle: String + let leaveAndCancelTitle: String + + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + leaveTitle = environment.strings.LiveStream_LeaveConfirmation + leaveAndCancelTitle = isScheduled ? environment.strings.LiveStream_LeaveAndCancelVoiceChat : environment.strings.LiveStream_LeaveAndEndVoiceChat + } else { + leaveTitle = environment.strings.VoiceChat_LeaveConfirmation + leaveAndCancelTitle = isScheduled ? environment.strings.VoiceChat_LeaveAndCancelVoiceChat : environment.strings.VoiceChat_LeaveAndEndVoiceChat + } + + items.append(ActionSheetTextItem(title: leaveTitle)) + items.append(ActionSheetButtonItem(title: leaveAndCancelTitle, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component, let environment = self.environment else { + return + } + let title: String + let text: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + title = isScheduled ? environment.strings.LiveStream_CancelConfirmationTitle : environment.strings.LiveStream_EndConfirmationTitle + text = isScheduled ? environment.strings.LiveStream_CancelConfirmationText : environment.strings.LiveStream_EndConfirmationText + } else { + title = isScheduled ? environment.strings.VoiceChat_CancelConfirmationTitle : environment.strings.VoiceChat_EndConfirmationTitle + text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText + } + + if let _ = self.members { + let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: { + action(true) + })]) + environment.controller()?.present(alertController, in: .window(.root)) + } else { + action(true) + } + })) + + let leaveText: String + if case let .channel(channel) = self.peer, case .broadcast = channel.info { + leaveText = environment.strings.LiveStream_LeaveVoiceChat + } else { + leaveText = environment.strings.VoiceChat_LeaveVoiceChat + } + + items.append(ActionSheetButtonItem(title: leaveText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + action(false) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + environment.controller()?.present(actionSheet, in: .window(.root)) + } else { + action(false) + } + } + func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -2473,6 +2563,7 @@ private final class VideoChatScreenComponent: Component { component: AnyComponent(VideoChatTitleComponent( title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? " ", status: idleTitleStatusText, + isRecording: self.callState?.recordingStartTimestamp != nil, strings: environment.strings )), environment: {}, @@ -2886,14 +2977,10 @@ private final class VideoChatScreenComponent: Component { )), effectAlignment: .center, action: { [weak self] in - guard let self, let component = self.component else { + guard let self else { return } - let _ = component.call.leave(terminateIfPossible: false).startStandalone() - - if let controller = self.environment?.controller() as? VideoChatScreenV2Impl { - controller.dismiss(closing: true, manual: false) - } + self.onLeavePressed() }, animateAlpha: false )), diff --git a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift index 93b9115a31..0f13e9d815 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatTitleComponent.swift @@ -4,19 +4,23 @@ import Display import ComponentFlow import MultilineTextComponent import TelegramPresentationData +import HierarchyTrackingLayer final class VideoChatTitleComponent: Component { let title: String let status: String + let isRecording: Bool let strings: PresentationStrings init( title: String, status: String, + isRecording: Bool, strings: PresentationStrings ) { self.title = title self.status = status + self.isRecording = isRecording self.strings = strings } @@ -27,6 +31,9 @@ final class VideoChatTitleComponent: Component { if lhs.status != rhs.status { return false } + if lhs.isRecording != rhs.isRecording { + return false + } if lhs.strings !== rhs.strings { return false } @@ -34,20 +41,46 @@ final class VideoChatTitleComponent: Component { } final class View: UIView { + private let hierarchyTrackingLayer: HierarchyTrackingLayer private let title = ComponentView() private var status: ComponentView? + private var recordingImageView: UIImageView? private var component: VideoChatTitleComponent? private var isUpdating: Bool = false override init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let self else { + return + } + self.updateAnimations() + } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func updateAnimations() { + if let recordingImageView = self.recordingImageView { + if recordingImageView.layer.animation(forKey: "blink") == nil { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber] + animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] + animation.duration = 0.7 + animation.autoreverses = true + animation.repeatCount = Float.infinity + recordingImageView.layer.add(animation, forKey: "blink") + } + } + } + func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -106,6 +139,32 @@ final class VideoChatTitleComponent: Component { statusView.bounds = CGRect(origin: CGPoint(), size: statusFrame.size) } + if component.isRecording { + var recordingImageTransition = transition + let recordingImageView: UIImageView + if let current = self.recordingImageView { + recordingImageView = current + } else { + recordingImageTransition = recordingImageTransition.withAnimation(.none) + recordingImageView = UIImageView() + recordingImageView.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xFF3B2F)) + self.recordingImageView = recordingImageView + self.addSubview(recordingImageView) + transition.animateScale(view: recordingImageView, from: 0.0001, to: 1.0) + } + let recordingImageFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 5.0, y: titleFrame.minY + floor(titleFrame.height - 8.0) * 0.5 + 1.0), size: CGSize(width: 8.0, height: 8.0)) + recordingImageTransition.setFrame(view: recordingImageView, frame: recordingImageFrame) + + self.updateAnimations() + } else { + if let recordingImageView = self.recordingImageView { + self.recordingImageView = nil + transition.setScale(view: recordingImageView, scale: 0.0001, completion: { [weak recordingImageView] _ in + recordingImageView?.removeFromSuperview() + }) + } + } + return size } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift b/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift index 8b81268148..364289cff1 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatVideoLoadingEffectView.swift @@ -8,125 +8,196 @@ 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 func generateGradient(baseAlpha: CGFloat) -> UIImage? { + return generateImage(CGSize(width: 200.0, 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() + } + }) +} + +private final class AnimatedGradientView: UIView { + private struct Params: Equatable { + var size: CGSize + var containerWidth: CGFloat + var offsetX: CGFloat + var gradientWidth: CGFloat + + init(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat) { + self.size = size + self.containerWidth = containerWidth + self.offsetX = offsetX + self.gradientWidth = gradientWidth + } + } + private let duration: Double private let hierarchyTrackingLayer: HierarchyTrackingLayer - private let gradientWidth: CGFloat - - let portalSource: PortalSourceView - + private let backgroundContainerView: UIView + private let backgroundScaleView: UIView + private let backgroundOffsetView: UIView private let backgroundView: UIImageView - private let borderGradientView: UIImageView - private let borderContainerView: UIView - let borderMaskLayer: SimpleShapeLayer + private var params: Params? - private var didPlayOnce = false - - init(effectAlpha: CGFloat, borderAlpha: CGFloat, gradientWidth: CGFloat = 200.0, duration: Double, hasCustomBorder: Bool, playOnce: Bool) { - self.portalSource = PortalSourceView() - + init(effectAlpha: CGFloat, duration: Double) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() self.duration = duration - self.hasCustomBorder = hasCustomBorder - self.playOnce = playOnce - self.gradientWidth = gradientWidth + self.backgroundContainerView = UIView() + self.backgroundContainerView.layer.anchorPoint = CGPoint() + + self.backgroundScaleView = UIView() + self.backgroundOffsetView = UIView() + self.backgroundView = UIImageView() - self.borderGradientView = UIImageView() - self.borderContainerView = UIView() - self.borderMaskLayer = SimpleShapeLayer() + super.init(frame: CGRect()) - super.init(frame: .zero) - - self.portalSource.backgroundColor = .red - - self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer) + self.layer.addSublayer(self.hierarchyTrackingLayer) self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in - guard let self, self.bounds.width != 0.0 else { + guard let self else { return } - self.updateAnimations(size: self.bounds.size) + self.updateAnimations() } - 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.backgroundView.image = generateGradient(baseAlpha: effectAlpha) - self.borderGradientView.image = generateGradient(borderAlpha) - self.borderContainerView.addSubview(self.borderGradientView) - self.portalSource.addSubview(self.borderContainerView) - self.borderContainerView.layer.mask = self.borderMaskLayer + self.backgroundOffsetView.addSubview(self.backgroundView) + self.backgroundScaleView.addSubview(self.backgroundOffsetView) + self.backgroundContainerView.addSubview(self.backgroundScaleView) + self.addSubview(self.backgroundContainerView) } 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 + private func updateAnimations() { + if self.backgroundView.layer.animation(forKey: "shimmer") == nil { + let animation = self.backgroundView.layer.makeAnimation(from: -1.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.backgroundView.layer.add(animation, forKey: "shimmer") + } + if self.backgroundScaleView.layer.animation(forKey: "shimmer") == nil { + let animation = self.backgroundScaleView.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.backgroundScaleView.layer.add(animation, forKey: "shimmer") } - - 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 - } + func update(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat, transition: ComponentTransition) { + let params = Params(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth) + if self.params == params { + return } + self.params = params - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height))) + let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)) + transition.setPosition(view: self.backgroundView, position: backgroundFrame.center) + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - 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))) + transition.setPosition(view: self.backgroundOffsetView, position: backgroundFrame.center) + transition.setBounds(view: self.backgroundOffsetView, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - self.updateAnimations(size: size) + transition.setTransform(view: self.backgroundOffsetView, transform: CATransform3DMakeScale(gradientWidth, 1.0, 1.0)) + + let backgroundContainerViewSubFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + transition.setPosition(view: self.backgroundContainerView, position: CGPoint()) + transition.setBounds(view: self.backgroundContainerView, bounds: backgroundContainerViewSubFrame) + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, -offsetX, 0.0, 0.0) + containerTransform = CATransform3DScale(containerTransform, containerWidth, size.height, 1.0) + transition.setSublayerTransform(view: self.backgroundContainerView, transform: containerTransform) + + transition.setSublayerTransform(view: self.backgroundScaleView, transform: CATransform3DMakeScale(1.0 / containerWidth, 1.0, 1.0)) + + self.updateAnimations() + } +} + +final class VideoChatVideoLoadingEffectView: UIView { + private struct Params: Equatable { + var size: CGSize + var containerWidth: CGFloat + var offsetX: CGFloat + var gradientWidth: CGFloat + + init(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat) { + self.size = size + self.containerWidth = containerWidth + self.offsetX = offsetX + self.gradientWidth = gradientWidth + } + } + + private let duration: Double + private let cornerRadius: CGFloat + + private let backgroundView: AnimatedGradientView + + private let borderMaskView: UIImageView + private let borderBackgroundView: AnimatedGradientView + + private var params: Params? + + init(effectAlpha: CGFloat, borderAlpha: CGFloat, cornerRadius: CGFloat = 12.0, duration: Double) { + self.duration = duration + self.cornerRadius = cornerRadius + + self.backgroundView = AnimatedGradientView(effectAlpha: effectAlpha, duration: duration) + + self.borderMaskView = UIImageView() + self.borderMaskView.image = generateStretchableFilledCircleImage(diameter: cornerRadius * 2.0, color: nil, strokeColor: .white, strokeWidth: 2.0) + self.borderBackgroundView = AnimatedGradientView(effectAlpha: borderAlpha, duration: duration) + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundView) + + self.borderBackgroundView.mask = self.borderMaskView + self.addSubview(self.borderBackgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, containerWidth: CGFloat, offsetX: CGFloat, gradientWidth: CGFloat, transition: ComponentTransition) { + let params = Params(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth) + if self.params == params { + return + } + self.params = params + + self.backgroundView.update(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth, transition: transition) + self.borderBackgroundView.update(size: size, containerWidth: containerWidth, offsetX: offsetX, gradientWidth: gradientWidth, transition: transition) + transition.setFrame(view: self.borderMaskView, frame: CGRect(origin: CGPoint(), size: size)) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 8e1a2c12be..533e8b89ca 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7098,15 +7098,14 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } private func calculateUseV2(context: AccountContext) -> Bool { - /*var useV2 = true + var useV2 = true if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] { useV2 = false } - return useV2*/ - return false + return useV2 } public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift index 1fcba0be18..da2744a7c3 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBlobsLayer.swift @@ -130,7 +130,7 @@ public final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject { let phase = self.phase let blobs = self.blobs - context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0)), edgeInset: 4), state: RenderState.self, layer: self, commands: { encoder, placement in + context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0)), edgeInset: 2), state: RenderState.self, layer: self, commands: { encoder, placement in let rect = placement.effectiveRect for i in 0 ..< blobs.count {