Video chat UI

This commit is contained in:
Isaac 2024-09-13 17:59:13 +08:00
parent 5e64c0ebd7
commit 63ac3822f7
10 changed files with 695 additions and 159 deletions

View File

@ -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<EnginePeer.Id>
let interfaceOrientation: UIInterfaceOrientation
let updateSelectedParticipant: (Participant.Key) -> Void
init(
@ -434,6 +445,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
participants: [Participant],
selectedParticipant: Participant.Key?,
speakingParticipants: Set<EnginePeer.Id>,
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

View File

@ -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)
}
}
})
}

View File

@ -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<Empty>()
private let title = ComponentView<Empty>()
@ -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<Empty>?
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<Empty>
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 {

View File

@ -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<Empty>()
private let title = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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<Empty>, transition: ComponentTransition) -> CGSize {

View File

@ -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<ViewControllerComponentContainer.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
)),

View File

@ -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<Empty>()
private var status: ComponentView<Empty>?
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<Empty>, 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
}
}

View File

@ -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))
}
}

View File

@ -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<Any, NoError> {

View File

@ -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 {