mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Video Chat Improvements
This commit is contained in:
parent
31067a651a
commit
659ea466c9
19 changed files with 2815 additions and 2706 deletions
|
|
@ -6465,8 +6465,12 @@ Sorry for the inconvenience.";
|
|||
"VoiceChat.VideoPreviewDescription" = "Are you sure you want to share your video?";
|
||||
"VoiceChat.VideoPreviewShareCamera" = "Share Camera Video";
|
||||
"VoiceChat.VideoPreviewShareScreen" = "Share Screen";
|
||||
"VoiceChat.VideoPreviewStopScreenSharing" = "Stop Screen Sharing";
|
||||
|
||||
"VoiceChat.TapToViewCameraVideo" = "Tap to view camera video";
|
||||
"VoiceChat.TapToViewScreenVideo" = "Tap to view screen sharing";
|
||||
|
||||
"VoiceChat.ShareScreen" = "Share Screen";
|
||||
"VoiceChat.StopScreenSharing" = "Stop Screen Sharing";
|
||||
|
||||
"WallpaperPreview.WallpaperColors" = "Colors";
|
||||
|
|
|
|||
|
|
@ -89,10 +89,23 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?,
|
|||
|
||||
public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize, font: UIFont = avatarPlaceholderFont(size: 13.0), fullSize: Bool = false) -> Signal<UIImage?, NoError> {
|
||||
let iconSignal: Signal<UIImage?, NoError>
|
||||
if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, inset: 0.0, emptyColor: nil, synchronousLoad: false) {
|
||||
iconSignal = signal
|
||||
if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) {
|
||||
if fullSize, let fullSizeSignal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) {
|
||||
iconSignal = combineLatest(.single(nil) |> then(signal), .single(nil) |> then(fullSizeSignal))
|
||||
|> mapToSignal { thumbnailImage, fullSizeImage -> Signal<UIImage?, NoError> in
|
||||
if let fullSizeImage = fullSizeImage {
|
||||
return .single(fullSizeImage.0)
|
||||
} else if let thumbnailImage = thumbnailImage {
|
||||
return .single(thumbnailImage.0)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
iconSignal = signal
|
||||
|> map { imageVersions -> UIImage? in
|
||||
return imageVersions?.0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let peerId = peer.id
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ private func edgeWidth(width: CGFloat) -> CGFloat {
|
|||
return min(44.0, floor(width / 6.0))
|
||||
}
|
||||
|
||||
private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in
|
||||
let fadeWidth: CGFloat = 70.0
|
||||
private let leftFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
|
|
@ -19,10 +20,10 @@ private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaq
|
|||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 64.0, y: 0.0), options: CGGradientDrawingOptions())
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})
|
||||
|
||||
private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in
|
||||
private let rightFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opa
|
|||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 64.0, y: 0.0), options: CGGradientDrawingOptions())
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})
|
||||
|
||||
public struct GalleryPagerInsertItem {
|
||||
|
|
@ -80,8 +81,8 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private let leftFadeNode: ASImageNode
|
||||
private let rightFadeNode: ASImageNode
|
||||
private let leftFadeNode: ASDisplayNode
|
||||
private let rightFadeNode: ASDisplayNode
|
||||
private var highlightedSide: Bool?
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
|
|
@ -117,15 +118,13 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.leftFadeNode = ASImageNode()
|
||||
self.leftFadeNode.contentMode = .scaleToFill
|
||||
self.leftFadeNode.image = leftFadeImage
|
||||
self.leftFadeNode = ASDisplayNode()
|
||||
self.leftFadeNode.alpha = 0.0
|
||||
self.leftFadeNode.backgroundColor = leftFadeImage.flatMap { UIColor(patternImage: $0) }
|
||||
|
||||
self.rightFadeNode = ASImageNode()
|
||||
self.rightFadeNode.contentMode = .scaleToFill
|
||||
self.rightFadeNode.image = rightFadeImage
|
||||
self.rightFadeNode = ASDisplayNode()
|
||||
self.rightFadeNode.alpha = 0.0
|
||||
self.rightFadeNode.backgroundColor = rightFadeImage.flatMap { UIColor(patternImage: $0) }
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -293,7 +292,6 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||
transition.animatePosition(node: centralItemNode, from: centralItemNode.position.offsetBy(dx: -updatedCentralPoint.x + centralPoint.x, dy: -updatedCentralPoint.y + centralPoint.y))
|
||||
}
|
||||
|
||||
let fadeWidth = min(72.0, layout.size.width * 0.2)
|
||||
self.leftFadeNode.frame = CGRect(x: 0.0, y: 0.0, width: fadeWidth, height: layout.size.height)
|
||||
self.rightFadeNode.frame = CGRect(x: layout.size.width - fadeWidth, y: 0.0, width: fadeWidth, height: layout.size.height)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -432,6 +432,8 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
private let fadeWidth: CGFloat = 70.0
|
||||
|
||||
public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
public var peer: Peer?
|
||||
|
|
@ -442,8 +444,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
public let shadowNode: ASImageNode
|
||||
|
||||
public let contentNode: ASDisplayNode
|
||||
let leftHighlightNode: ASImageNode
|
||||
let rightHighlightNode: ASImageNode
|
||||
let leftHighlightNode: ASDisplayNode
|
||||
let rightHighlightNode: ASDisplayNode
|
||||
var highlightedSide: Bool?
|
||||
public let stripContainerNode: ASDisplayNode
|
||||
public let highlightContainerNode: ASDisplayNode
|
||||
|
|
@ -564,11 +566,9 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
self.leftHighlightNode = ASImageNode()
|
||||
self.leftHighlightNode = ASDisplayNode()
|
||||
self.leftHighlightNode.displaysAsynchronously = false
|
||||
self.leftHighlightNode.displayWithoutProcessing = true
|
||||
self.leftHighlightNode.contentMode = .scaleToFill
|
||||
self.leftHighlightNode.image = generateImage(CGSize(width: 88.0, height: 1.0), contextGenerator: { size, context in
|
||||
self.leftHighlightNode.backgroundColor = generateImage(CGSize(width: fadeWidth, height: 24.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let topColor = UIColor(rgb: 0x000000, alpha: 0.1)
|
||||
|
|
@ -581,14 +581,12 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
||||
})
|
||||
}).flatMap { UIColor(patternImage: $0) }
|
||||
self.leftHighlightNode.alpha = 0.0
|
||||
|
||||
self.rightHighlightNode = ASImageNode()
|
||||
self.rightHighlightNode = ASDisplayNode()
|
||||
self.rightHighlightNode.displaysAsynchronously = false
|
||||
self.rightHighlightNode.displayWithoutProcessing = true
|
||||
self.rightHighlightNode.contentMode = .scaleToFill
|
||||
self.rightHighlightNode.image = generateImage(CGSize(width: 88.0, height: 1.0), contextGenerator: { size, context in
|
||||
self.rightHighlightNode.backgroundColor = generateImage(CGSize(width: fadeWidth, height: 24.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let topColor = UIColor(rgb: 0x000000, alpha: 0.1)
|
||||
|
|
@ -601,7 +599,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: 0.0, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
||||
})
|
||||
}).flatMap { UIColor(patternImage: $0) }
|
||||
self.rightHighlightNode.alpha = 0.0
|
||||
|
||||
self.stripContainerNode = ASDisplayNode()
|
||||
|
|
@ -963,8 +961,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
if !isExpanded && previousExpanded {
|
||||
self.isCollapsing = true
|
||||
}
|
||||
self.leftHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width * 1.0 / 5.0), height: size.height))
|
||||
self.rightHighlightNode.frame = CGRect(origin: CGPoint(x: size.width - floor(size.width * 1.0 / 5.0), y: 0.0), size: CGSize(width: floor(size.width * 1.0 / 5.0), height: size.height))
|
||||
self.leftHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: fadeWidth, height: size.height))
|
||||
self.rightHighlightNode.frame = CGRect(origin: CGPoint(x: size.width - fadeWidth, y: 0.0), size: CGSize(width: fadeWidth, height: size.height))
|
||||
|
||||
if let peer = peer, !self.initializedList {
|
||||
self.initializedList = true
|
||||
|
|
|
|||
|
|
@ -71,12 +71,15 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
private var statusNode: SemanticStatusNode?
|
||||
let textNode: ImmediateTextNode
|
||||
|
||||
private let largeButtonSize: CGFloat = 72.0
|
||||
private let largeButtonSize: CGFloat
|
||||
|
||||
private var size: CGSize?
|
||||
private(set) var currentContent: Content?
|
||||
private(set) var currentText: String = ""
|
||||
|
||||
init() {
|
||||
init(largeButtonSize: CGFloat = 72.0) {
|
||||
self.largeButtonSize = largeButtonSize
|
||||
|
||||
self.wrapperNode = ASDisplayNode()
|
||||
self.contentContainer = ASDisplayNode()
|
||||
|
||||
|
|
@ -145,9 +148,10 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
|
||||
if self.currentContent != content {
|
||||
if self.currentContent != content || self.size != size {
|
||||
let previousContent = self.currentContent
|
||||
self.currentContent = content
|
||||
self.size = size
|
||||
|
||||
if content.hasProgress {
|
||||
let statusFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
||||
|
|
@ -201,7 +205,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
self.contentContainer.insertSubnode(animationNode, aboveSubnode: self.contentNode)
|
||||
}
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = animationFrame
|
||||
animationNode.bounds = animationFrame
|
||||
animationNode.position = CGPoint(x: self.largeButtonSize / 2.0, y: self.largeButtonSize / 2.0)
|
||||
if previousContent == nil {
|
||||
animationNode.seekToEnd()
|
||||
} else if previousContent?.image != content.image {
|
||||
|
|
@ -371,6 +376,9 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||
|
||||
transition.updatePosition(node: self.contentContainer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
transition.updateSublayerTransformScale(node: self.contentContainer, scale: scaleFactor)
|
||||
if let animationNode = self.animationNode {
|
||||
transition.updateTransformScale(node: animationNode, scale: isSmall ? 1.35 : 1.12)
|
||||
}
|
||||
|
||||
if self.currentText != text {
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: .white)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import AccountContext
|
|||
import LegacyComponents
|
||||
import AnimatedCountLabelNode
|
||||
|
||||
private let blue = UIColor(rgb: 0x0078ff)
|
||||
private let lightBlue = UIColor(rgb: 0x59c7f8)
|
||||
private let blue = UIColor(rgb: 0x007fff)
|
||||
private let lightBlue = UIColor(rgb: 0x00affe)
|
||||
private let green = UIColor(rgb: 0x33c659)
|
||||
private let activeBlue = UIColor(rgb: 0x00a0b9)
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
|
|
|
|||
|
|
@ -130,11 +130,11 @@ final class GroupVideoNode: ASDisplayNode {
|
|||
}
|
||||
UIView.transition(with: withBackground ? self.videoViewContainer : self.view, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: {
|
||||
UIView.performWithoutAnimation {
|
||||
self.updateIsBlurred(isBlurred: true, light: true, animated: false)
|
||||
self.updateIsBlurred(isBlurred: true, light: false, animated: false)
|
||||
}
|
||||
}) { finished in
|
||||
self.backgroundColor = nil
|
||||
Queue.mainQueue().after(0.5) {
|
||||
Queue.mainQueue().after(0.4) {
|
||||
self.updateIsBlurred(isBlurred: false)
|
||||
}
|
||||
}
|
||||
|
|
@ -202,7 +202,8 @@ final class GroupVideoNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
var rotatedVideoSize = CGSize(width: 100.0, height: rotatedAspect * 100.0)
|
||||
|
||||
let videoSize = rotatedVideoSize
|
||||
|
||||
var containerSize = size
|
||||
if switchOrientation {
|
||||
rotatedVideoSize = CGSize(width: rotatedVideoSize.height, height: rotatedVideoSize.width)
|
||||
|
|
@ -217,13 +218,13 @@ final class GroupVideoNode: ASDisplayNode {
|
|||
case .fillOrFitToSquare:
|
||||
rotatedVideoSize = filledToSquareSize
|
||||
case .fillHorizontal:
|
||||
if rotatedVideoSize.width > rotatedVideoSize.height {
|
||||
if videoSize.width > videoSize.height {
|
||||
rotatedVideoSize = filledSize
|
||||
} else {
|
||||
rotatedVideoSize = fittedSize
|
||||
}
|
||||
case .fillVertical:
|
||||
if rotatedVideoSize.width < rotatedVideoSize.height {
|
||||
if videoSize.width < videoSize.height {
|
||||
rotatedVideoSize = filledSize
|
||||
} else {
|
||||
rotatedVideoSize = fittedSize
|
||||
|
|
@ -236,11 +237,11 @@ final class GroupVideoNode: ASDisplayNode {
|
|||
rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width)
|
||||
rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height)
|
||||
|
||||
let videoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0))
|
||||
let normalizedVideoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0))
|
||||
transition.updatePosition(layer: self.videoView.view.layer, position: rotatedVideoFrame.center)
|
||||
transition.updateBounds(layer: self.videoView.view.layer, bounds: CGRect(origin: CGPoint(), size: videoSize))
|
||||
transition.updateBounds(layer: self.videoView.view.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize))
|
||||
|
||||
let transformScale: CGFloat = rotatedVideoFrame.width / videoSize.width
|
||||
let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width
|
||||
transition.updateTransformScale(layer: self.videoViewContainer.layer, scale: transformScale)
|
||||
|
||||
if let backdropVideoView = self.backdropVideoView {
|
||||
|
|
@ -251,11 +252,11 @@ final class GroupVideoNode: ASDisplayNode {
|
|||
rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width)
|
||||
rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height)
|
||||
|
||||
let videoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0))
|
||||
let normalizedVideoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0))
|
||||
transition.updatePosition(layer: backdropVideoView.view.layer, position: rotatedVideoFrame.center)
|
||||
transition.updateBounds(layer: backdropVideoView.view.layer, bounds: CGRect(origin: CGPoint(), size: videoSize))
|
||||
transition.updateBounds(layer: backdropVideoView.view.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize))
|
||||
|
||||
let transformScale: CGFloat = rotatedVideoFrame.width / videoSize.width
|
||||
let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width
|
||||
transition.updateTransformScale(layer: self.backdropVideoViewContainer.layer, scale: transformScale)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
|
|
@ -263,10 +264,26 @@ final class GroupVideoNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
if let backdropEffectView = self.backdropEffectView {
|
||||
let maxSide = max(bounds.width, bounds.height) * 2.0
|
||||
let squareBounds = CGRect(x: (bounds.width - maxSide) / 2.0, y: (bounds.width - maxSide) / 2.0, width: maxSide, height: maxSide)
|
||||
transition.animateView {
|
||||
backdropEffectView.frame = squareBounds
|
||||
let maxSide = max(bounds.width, bounds.height) + 32.0
|
||||
let squareBounds = CGRect(x: (bounds.width - maxSide) / 2.0, y: (bounds.height - maxSide) / 2.0, width: maxSide, height: maxSide)
|
||||
|
||||
if case let .animated(duration, .spring) = transition {
|
||||
if false, #available(iOS 10.0, *) {
|
||||
let timing = UISpringTimingParameters(mass: 3.0, stiffness: 1000.0, damping: 500.0, initialVelocity: CGVector(dx: 0.0, dy: 0.0))
|
||||
let animator = UIViewPropertyAnimator(duration: 0.34, timingParameters: timing)
|
||||
animator.addAnimations {
|
||||
backdropEffectView.frame = squareBounds
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: {
|
||||
backdropEffectView.frame = squareBounds
|
||||
})
|
||||
}
|
||||
} else {
|
||||
transition.animateView {
|
||||
backdropEffectView.frame = squareBounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2512,15 +2512,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
}
|
||||
|
||||
private func requestScreencast() {
|
||||
if self.screencastCallContext != nil {
|
||||
return
|
||||
}
|
||||
|
||||
let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo
|
||||
|
||||
guard let callInfo = maybeCallInfo else {
|
||||
guard let callInfo = self.internalState.callInfo, self.screencastCallContext == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
self.hasScreencast = true
|
||||
|
||||
let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, audioStreamData: nil, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false)
|
||||
self.screencastCallContext = screencastCallContext
|
||||
|
|
@ -2553,109 +2549,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
/*if self.screencastIpcContext != nil {
|
||||
return
|
||||
}
|
||||
|
||||
let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo
|
||||
|
||||
guard let callInfo = maybeCallInfo else {
|
||||
return
|
||||
}
|
||||
|
||||
let screencastIpcContext = IpcGroupCallAppContext(basePath: self.accountContext.sharedContext.basePath + "/broadcast-coordination")
|
||||
self.screencastIpcContext = screencastIpcContext
|
||||
self.hasScreencast = true
|
||||
|
||||
self.screencastJoinDisposable.set((screencastIpcContext.joinPayload
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.requestDisposable.set((joinGroupCallAsScreencast(
|
||||
account: strongSelf.account,
|
||||
peerId: strongSelf.peerId,
|
||||
callId: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
joinPayload: joinPayload
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||
guard let strongSelf = self, let screencastIpcContext = strongSelf.screencastIpcContext else {
|
||||
return
|
||||
}
|
||||
let clientParams = joinCallResult.jsonParams
|
||||
|
||||
screencastIpcContext.setJoinResponsePayload(clientParams)
|
||||
|
||||
strongSelf.genericCallContext?.setIgnoreVideoEndpointIds(endpointIds: [joinCallResult.endpointId])
|
||||
}, error: { error in
|
||||
guard let _ = self else {
|
||||
return
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
/*if self.screenCapturer == nil {
|
||||
let screenCapturer = OngoingCallVideoCapturer()
|
||||
self.screenCapturer = screenCapturer
|
||||
}
|
||||
|
||||
let screencastCallContext = OngoingGroupCallContext(
|
||||
video: self.screenCapturer,
|
||||
requestMediaChannelDescriptions: { _, completion in
|
||||
completion([])
|
||||
return EmptyDisposable
|
||||
},
|
||||
audioStreamData: nil,
|
||||
rejoinNeeded: {},
|
||||
outgoingAudioBitrateKbit: nil,
|
||||
videoContentType: .screencast,
|
||||
enableNoiseSuppression: false
|
||||
)
|
||||
|
||||
self.screencastCallContext = screencastCallContext
|
||||
|
||||
self.screencastJoinDisposable.set((screencastCallContext.joinPayload
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
if lhs.0 != rhs.0 {
|
||||
return false
|
||||
}
|
||||
if lhs.1 != rhs.1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.requestDisposable.set((joinGroupCallAsScreencast(
|
||||
account: strongSelf.account,
|
||||
peerId: strongSelf.peerId,
|
||||
callId: callInfo.id,
|
||||
accessHash: callInfo.accessHash,
|
||||
joinPayload: joinPayload
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||
guard let strongSelf = self, let screencastCallContext = strongSelf.screencastCallContext else {
|
||||
return
|
||||
}
|
||||
let clientParams = joinCallResult.jsonParams
|
||||
|
||||
screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
|
||||
screencastCallContext.setJoinResponse(payload: clientParams)
|
||||
|
||||
strongSelf.genericCallContext?.setIgnoreVideoEndpointIds(endpointIds: [joinCallResult.endpointId])
|
||||
}, error: { error in
|
||||
guard let _ = self else {
|
||||
return
|
||||
}
|
||||
}))
|
||||
}))*/*/
|
||||
}
|
||||
|
||||
public func disableScreencast() {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ private let subtitleFont = Font.regular(13.0)
|
|||
private let white = UIColor(rgb: 0xffffff)
|
||||
private let greyColor = UIColor(rgb: 0x2c2c2e)
|
||||
private let secondaryGreyColor = UIColor(rgb: 0x1c1c1e)
|
||||
private let blue = UIColor(rgb: 0x0078ff)
|
||||
private let lightBlue = UIColor(rgb: 0x59c7f8)
|
||||
private let blue = UIColor(rgb: 0x007fff)
|
||||
private let lightBlue = UIColor(rgb: 0x00affe)
|
||||
private let green = UIColor(rgb: 0x33c659)
|
||||
private let activeBlue = UIColor(rgb: 0x00a0b9)
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
|
|
@ -607,7 +607,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||
|
||||
self.foregroundGradientLayer.type = .radial
|
||||
self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.55, 1.0]
|
||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
|
|
@ -798,12 +798,12 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||
case .speaking:
|
||||
targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor]
|
||||
targetScale = 0.89
|
||||
outerColor = UIColor(rgb: 0x21674f)
|
||||
outerColor = UIColor(rgb: 0x134b22)
|
||||
activeColor = green
|
||||
case .active:
|
||||
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
|
||||
targetScale = 0.85
|
||||
outerColor = UIColor(rgb: 0x1d588d)
|
||||
outerColor = UIColor(rgb: 0x002e5d)
|
||||
activeColor = blue
|
||||
case .connecting:
|
||||
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
|
||||
|
|
@ -1296,8 +1296,8 @@ private final class VoiceBlobView: UIView {
|
|||
}
|
||||
|
||||
public func setColor(_ color: UIColor) {
|
||||
mediumBlob.setColor(color.withAlphaComponent(0.55))
|
||||
bigBlob.setColor(color.withAlphaComponent(0.35))
|
||||
mediumBlob.setColor(color.withAlphaComponent(0.5))
|
||||
bigBlob.setColor(color.withAlphaComponent(0.21))
|
||||
}
|
||||
|
||||
public func updateLevel(_ level: CGFloat) {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
|
||||
private var applicationStateDisposable: Disposable?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var shareCamera: ((Bool) -> Void)?
|
||||
var switchCamera: (() -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
|
|
@ -298,11 +300,13 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
}
|
||||
|
||||
@objc private func microphonePressed() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.microphoneButton.isSelected = !self.microphoneButton.isSelected
|
||||
self.microphoneIconNode.update(state: .init(muted: !self.microphoneButton.isSelected, filled: true, color: .white), animated: true)
|
||||
}
|
||||
|
||||
@objc private func switchCameraPressed() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.switchCamera?()
|
||||
|
||||
let springDuration: Double = 0.7
|
||||
|
|
|
|||
|
|
@ -70,15 +70,18 @@ func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
|
|||
}
|
||||
|
||||
private func decorationBottomGradientImage(dark: Bool) -> UIImage? {
|
||||
return generateImage(CGSize(width: 1.0, height: bottomGradientHeight), rotatedContext: { size, context in
|
||||
return generateImage(CGSize(width: 24.0, height: bottomGradientHeight), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let color = dark ? fullscreenBackgroundColor : panelBackgroundColor
|
||||
let colorsArray = [color.withAlphaComponent(0.0).cgColor, color.cgColor] as CFArray
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
//
|
||||
// context.setFillColor(UIColor.red.cgColor)
|
||||
// context.fill(bounds)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -676,9 +679,8 @@ public final class VoiceChatController: ViewController {
|
|||
private let closeButton: VoiceChatHeaderButton
|
||||
private let topCornersNode: ASImageNode
|
||||
private let videoBottomCornersNode: ASImageNode
|
||||
private let bottomPanelCoverNode: ASDisplayNode
|
||||
fileprivate let bottomPanelNode: ASDisplayNode
|
||||
private let bottomGradientNode: ASImageNode
|
||||
private let bottomGradientNode: ASDisplayNode
|
||||
private let bottomPanelBackgroundNode: ASDisplayNode
|
||||
private let bottomCornersNode: ASImageNode
|
||||
fileprivate let audioButton: CallControllerButtonItemNode
|
||||
|
|
@ -693,8 +695,10 @@ public final class VoiceChatController: ViewController {
|
|||
private let mainStageNode: VoiceChatMainStageNode
|
||||
|
||||
private let transitionMaskView: UIView
|
||||
private let transitionMaskTopFillLayer: CALayer
|
||||
private let transitionMaskFillLayer: CALayer
|
||||
private let transitionMaskFillGradientLayer: CAGradientLayer
|
||||
private let transitionMaskGradientLayer: CAGradientLayer
|
||||
private let transitionMaskBottomFillLayer: CALayer
|
||||
private let transitionContainerNode: ASDisplayNode
|
||||
|
||||
private var isScheduling = false
|
||||
|
|
@ -791,10 +795,10 @@ public final class VoiceChatController: ViewController {
|
|||
private var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = []
|
||||
|
||||
private var videoNodes: [String: GroupVideoNode] = [:]
|
||||
private var wideVideoNodes = Set<String>()
|
||||
private var videoOrder: [String] = []
|
||||
private var readyVideoNodes = Set<String>()
|
||||
private var readyVideoDisposables = DisposableDict<String>()
|
||||
private var wideVideoNodes = Set<String>()
|
||||
private var videoNodesOrder: [String] = []
|
||||
|
||||
private var endpointToPeerId: [String: PeerId] = [:]
|
||||
private var peerIdToEndpoint: [PeerId: String] = [:]
|
||||
|
|
@ -906,9 +910,6 @@ public final class VoiceChatController: ViewController {
|
|||
self.topCornersNode.displaysAsynchronously = false
|
||||
self.topCornersNode.displayWithoutProcessing = true
|
||||
self.topCornersNode.image = decorationCornersImage(top: true, bottom: false, dark: false)
|
||||
|
||||
self.bottomPanelCoverNode = ASDisplayNode()
|
||||
self.bottomPanelCoverNode.backgroundColor = fullscreenBackgroundColor
|
||||
|
||||
self.bottomPanelNode = ASDisplayNode()
|
||||
self.bottomPanelNode.clipsToBounds = false
|
||||
|
|
@ -917,10 +918,9 @@ public final class VoiceChatController: ViewController {
|
|||
self.bottomPanelBackgroundNode.backgroundColor = panelBackgroundColor
|
||||
self.bottomPanelBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.bottomGradientNode = ASImageNode()
|
||||
self.bottomGradientNode = ASDisplayNode()
|
||||
self.bottomGradientNode.displaysAsynchronously = false
|
||||
self.bottomGradientNode.contentMode = .scaleToFill
|
||||
self.bottomGradientNode.image = decorationBottomGradientImage(dark: false)
|
||||
self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: false).flatMap { UIColor(patternImage: $0) }
|
||||
|
||||
self.bottomCornersNode = ASImageNode()
|
||||
self.bottomCornersNode.displaysAsynchronously = false
|
||||
|
|
@ -935,7 +935,7 @@ public final class VoiceChatController: ViewController {
|
|||
self.videoBottomCornersNode.isUserInteractionEnabled = false
|
||||
|
||||
self.audioButton = CallControllerButtonItemNode()
|
||||
self.cameraButton = CallControllerButtonItemNode()
|
||||
self.cameraButton = CallControllerButtonItemNode(largeButtonSize: sideButtonSize.width)
|
||||
self.switchCameraButton = CallControllerButtonItemNode()
|
||||
self.switchCameraButton.alpha = 0.0
|
||||
self.switchCameraButton.isUserInteractionEnabled = false
|
||||
|
|
@ -967,24 +967,34 @@ public final class VoiceChatController: ViewController {
|
|||
self.mainStageContainerNode.isHidden = true
|
||||
|
||||
self.mainStageBackgroundNode = ASDisplayNode()
|
||||
// self.mainStageBackgroundNode.backgroundColor = .black
|
||||
self.mainStageBackgroundNode.backgroundColor = .black
|
||||
self.mainStageBackgroundNode.alpha = 0.0
|
||||
self.mainStageBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.mainStageNode = VoiceChatMainStageNode(context: self.context, call: self.call)
|
||||
|
||||
self.transitionMaskView = UIView()
|
||||
self.transitionMaskTopFillLayer = CALayer()
|
||||
self.transitionMaskTopFillLayer.backgroundColor = UIColor.white.cgColor
|
||||
self.transitionMaskTopFillLayer.opacity = 0.0
|
||||
|
||||
self.transitionMaskFillLayer = CALayer()
|
||||
self.transitionMaskFillLayer.backgroundColor = UIColor.white.cgColor
|
||||
|
||||
self.transitionMaskGradientLayer = CAGradientLayer()
|
||||
self.transitionMaskGradientLayer.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
|
||||
self.transitionMaskGradientLayer.locations = [0.0, 1.0]
|
||||
self.transitionMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
self.transitionMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.transitionMaskFillGradientLayer = CAGradientLayer()
|
||||
self.transitionMaskFillGradientLayer.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
|
||||
self.transitionMaskFillGradientLayer.locations = [0.0, 1.0]
|
||||
self.transitionMaskFillGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
self.transitionMaskFillGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
self.transitionMaskBottomFillLayer = CALayer()
|
||||
self.transitionMaskBottomFillLayer.backgroundColor = UIColor.white.cgColor
|
||||
self.transitionMaskBottomFillLayer.opacity = 0.0
|
||||
|
||||
self.transitionMaskView.layer.addSublayer(self.transitionMaskTopFillLayer)
|
||||
self.transitionMaskView.layer.addSublayer(self.transitionMaskFillLayer)
|
||||
self.transitionMaskView.layer.addSublayer(self.transitionMaskFillGradientLayer)
|
||||
self.transitionMaskView.layer.addSublayer(self.transitionMaskGradientLayer)
|
||||
self.transitionMaskView.layer.addSublayer(self.transitionMaskBottomFillLayer)
|
||||
|
||||
self.transitionContainerNode = ASDisplayNode()
|
||||
self.transitionContainerNode.clipsToBounds = true
|
||||
|
|
@ -1657,20 +1667,16 @@ public final class VoiceChatController: ViewController {
|
|||
self.contentContainer.addSubnode(self.topPanelNode)
|
||||
self.contentContainer.addSubnode(self.leftBorderNode)
|
||||
self.contentContainer.addSubnode(self.rightBorderNode)
|
||||
// self.contentContainer.addSubnode(self.bottomPanelCoverNode)
|
||||
self.contentContainer.addSubnode(self.bottomCornersNode)
|
||||
self.contentContainer.addSubnode(self.bottomGradientNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelBackgroundNode)
|
||||
self.contentContainer.addSubnode(self.mainStageContainerNode)
|
||||
self.contentContainer.addSubnode(self.transitionContainerNode)
|
||||
self.contentContainer.addSubnode(self.bottomPanelNode)
|
||||
self.contentContainer.addSubnode(self.mainStageContainerNode)
|
||||
self.contentContainer.addSubnode(self.timerNode)
|
||||
self.contentContainer.addSubnode(self.scheduleTextNode)
|
||||
self.contentContainer.addSubnode(self.fullscreenListNode)
|
||||
|
||||
|
||||
|
||||
|
||||
self.mainStageContainerNode.addSubnode(self.mainStageBackgroundNode)
|
||||
self.mainStageContainerNode.addSubnode(self.mainStageNode)
|
||||
|
||||
|
|
@ -1715,6 +1721,7 @@ public final class VoiceChatController: ViewController {
|
|||
|
||||
if strongSelf.callState != state {
|
||||
strongSelf.callState = state
|
||||
strongSelf.mainStageNode.callState = state
|
||||
|
||||
if let muteState = state.muteState, !muteState.canUnmute {
|
||||
if strongSelf.pushingToTalk {
|
||||
|
|
@ -1829,10 +1836,10 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
if case .fullscreen = strongSelf.displayMode {
|
||||
if case .fullscreen = strongSelf.displayMode, !strongSelf.mainStageNode.animating {
|
||||
if let (peerId, _) = maxLevelWithVideo {
|
||||
if let peer = strongSelf.currentDominantSpeaker, CACurrentMediaTime() - peer.1 < 2.5 {
|
||||
} else {
|
||||
} else if strongSelf.currentDominantSpeaker?.0 != peerId {
|
||||
strongSelf.currentDominantSpeaker = (peerId, CACurrentMediaTime())
|
||||
strongSelf.updateMainVideo(waitForFullSize: false)
|
||||
}
|
||||
|
|
@ -1878,7 +1885,7 @@ public final class VoiceChatController: ViewController {
|
|||
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
|
||||
if let strongSelf = self {
|
||||
strongSelf.currentContentOffset = offset
|
||||
if !(strongSelf.animatingExpansion || strongSelf.animatingInsertion || strongSelf.animatingAppearance) && strongSelf.panGestureArguments == nil {
|
||||
if !(strongSelf.animatingExpansion || strongSelf.animatingInsertion || strongSelf.animatingAppearance) && (strongSelf.panGestureArguments == nil || strongSelf.isExpanded) {
|
||||
strongSelf.updateDecorationsLayout(transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -2098,9 +2105,19 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
if #available(iOS 12.0, *) {
|
||||
items.append(.custom(VoiceChatShareScreenContextItem(context: strongSelf.context, text: strongSelf.presentationData.strings.VoiceChat_VideoPreviewShareScreen, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, _ in }), false))
|
||||
if strongSelf.call.hasScreencast {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StopScreenSharing, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
self?.call.disableScreencast()
|
||||
})))
|
||||
} else {
|
||||
items.append(.custom(VoiceChatShareScreenContextItem(context: strongSelf.context, text: strongSelf.presentationData.strings.VoiceChat_ShareScreen, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, _ in }), false))
|
||||
}
|
||||
}
|
||||
|
||||
if canManageCall {
|
||||
|
|
@ -2989,6 +3006,7 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
@objc private func cameraPressed() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
if self.call.hasVideo || self.call.hasScreencast {
|
||||
self.call.disableVideo()
|
||||
self.call.disableScreencast()
|
||||
|
|
@ -3014,7 +3032,9 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
}, switchCamera: { [weak self] in
|
||||
self?.call.switchVideoCamera()
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self?.call.switchVideoCamera()
|
||||
}
|
||||
})
|
||||
strongSelf.controller?.present(controller, in: .window(.root))
|
||||
}
|
||||
|
|
@ -3022,7 +3042,24 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
@objc private func switchCameraPressed() {
|
||||
self.call.switchVideoCamera()
|
||||
self.hapticFeedback.impact(.light)
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.call.switchVideoCamera()
|
||||
}
|
||||
|
||||
if let callState = self.callState {
|
||||
for entry in self.currentFullscreenEntries {
|
||||
if case let .peer(peerEntry, _) = entry {
|
||||
if peerEntry.peer.id == callState.myPeerId {
|
||||
if let videoEndpointId = peerEntry.videoEndpointId, let videoNode = self.videoNodes[videoEndpointId] {
|
||||
videoNode.flip(withBackground: false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mainStageNode.flipVideoIfNeeded()
|
||||
|
||||
let springDuration: Double = 0.7
|
||||
let springDamping: CGFloat = 100.0
|
||||
|
|
@ -3132,22 +3169,21 @@ public final class VoiceChatController: ViewController {
|
|||
|
||||
let sideInset: CGFloat = 14.0
|
||||
|
||||
|
||||
let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
let bottomGradientFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomGradientHeight))
|
||||
|
||||
let topEdgeY = topPanelFrame.maxY
|
||||
var bottomEdgeY = self.isFullscreen ? layout.size.height - (self.isLandscape ? 0.0 : effectiveBottomAreaHeight) : bottomGradientFrame.maxY
|
||||
if case .modal(_, true) = self.effectiveDisplayMode {
|
||||
bottomEdgeY += 36.0
|
||||
}
|
||||
|
||||
let transitionContainerFrame = CGRect(x: sideInset, y: topEdgeY, width: layout.size.width - sideInset * 2.0, height: max(0.0, bottomEdgeY - topEdgeY))
|
||||
let additionalMaskHeight: CGFloat = 62.0
|
||||
let topEdgeY = isLandscape ? 0.0 : layoutTopInset
|
||||
let bottomEdgeY = (self.isLandscape ? layout.size.height + bottomGradientHeight : bottomGradientFrame.maxY) + additionalMaskHeight
|
||||
|
||||
let transitionContainerFrame = CGRect(x: 0.0, y: topEdgeY, width: layout.size.width, height: max(0.0, bottomEdgeY - topEdgeY))
|
||||
transition.updateFrame(node: self.transitionContainerNode, frame: transitionContainerFrame)
|
||||
transition.updateFrame(view: self.transitionMaskView, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: transitionContainerFrame.height))
|
||||
let updateMaskLayers = {
|
||||
transition.updateFrame(layer: self.transitionMaskFillLayer, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: transitionContainerFrame.height - bottomGradientHeight))
|
||||
transition.updateFrame(layer: self.transitionMaskFillGradientLayer, frame: CGRect(x: 0.0, y: transitionContainerFrame.height - bottomGradientHeight, width: transitionContainerFrame.width, height: bottomGradientHeight))
|
||||
transition.updateFrame(layer: self.transitionMaskTopFillLayer, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: topPanelFrame.height))
|
||||
transition.updateFrame(layer: self.transitionMaskFillLayer, frame: CGRect(x: 0.0, y: topPanelFrame.height, width: transitionContainerFrame.width, height: transitionContainerFrame.height - bottomGradientHeight - additionalMaskHeight - topPanelFrame.height))
|
||||
transition.updateFrame(layer: self.transitionMaskGradientLayer, frame: CGRect(x: 0.0, y: transitionContainerFrame.height - bottomGradientHeight - additionalMaskHeight, width: transitionContainerFrame.width, height: bottomGradientHeight))
|
||||
transition.updateFrame(layer: self.transitionMaskBottomFillLayer, frame: CGRect(x: 0.0, y: transitionContainerFrame.height - bottomGradientHeight - additionalMaskHeight, width: transitionContainerFrame.width, height: bottomGradientHeight + additionalMaskHeight))
|
||||
}
|
||||
if transition.isAnimated {
|
||||
updateMaskLayers()
|
||||
|
|
@ -3336,7 +3372,7 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
UIView.transition(with: self.bottomGradientNode.view, duration: 0.3, options: [.transitionCrossDissolve, .curveLinear]) {
|
||||
self.bottomGradientNode.image = decorationBottomGradientImage(dark: isFullscreen)
|
||||
self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: isFullscreen).flatMap { UIColor(patternImage: $0) }
|
||||
} completion: { _ in
|
||||
}
|
||||
|
||||
|
|
@ -3476,7 +3512,7 @@ public final class VoiceChatController: ViewController {
|
|||
buttonsTitleAlpha = 0.0
|
||||
}
|
||||
|
||||
let hasVideo = self.call.hasVideo || self.call.hasScreencast
|
||||
let hasVideo = self.call.hasVideo
|
||||
self.cameraButton.update(size: hasVideo ? sideButtonSize : videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition)
|
||||
|
||||
self.switchCameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition)
|
||||
|
|
@ -3627,13 +3663,11 @@ public final class VoiceChatController: ViewController {
|
|||
|
||||
var bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
|
||||
let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
|
||||
let bottomPanelCoverFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomPanelCoverHeight))
|
||||
if isLandscape {
|
||||
bottomPanelFrame = CGRect(origin: CGPoint(x: layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right, y: 0.0), size: CGSize(width: fullscreenBottomAreaHeight + layout.safeInsets.right, height: layout.size.height))
|
||||
}
|
||||
let bottomGradientFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomGradientHeight))
|
||||
transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0)
|
||||
transition.updateFrame(node: self.bottomPanelCoverNode, frame: bottomPanelCoverFrame)
|
||||
transition.updateFrame(node: self.bottomGradientNode, frame: bottomGradientFrame)
|
||||
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
|
||||
|
||||
|
|
@ -4155,8 +4189,8 @@ public final class VoiceChatController: ViewController {
|
|||
var isTile = false
|
||||
if let interaction = self.itemInteraction {
|
||||
if let videoEndpointId = member.presentationEndpointId, self.readyVideoNodes.contains(videoEndpointId) {
|
||||
if !self.videoNodesOrder.contains(videoEndpointId) {
|
||||
self.videoNodesOrder.append(videoEndpointId)
|
||||
if !self.videoOrder.contains(videoEndpointId) {
|
||||
self.videoOrder.append(videoEndpointId)
|
||||
}
|
||||
if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId) {
|
||||
isTile = true
|
||||
|
|
@ -4167,8 +4201,8 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
if let videoEndpointId = member.videoEndpointId, self.readyVideoNodes.contains(videoEndpointId) {
|
||||
if !self.videoNodesOrder.contains(videoEndpointId) {
|
||||
self.videoNodesOrder.append(videoEndpointId)
|
||||
if !self.videoOrder.contains(videoEndpointId) {
|
||||
self.videoOrder.append(videoEndpointId)
|
||||
}
|
||||
if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId) {
|
||||
isTile = true
|
||||
|
|
@ -4204,18 +4238,18 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
var preList: [String] = []
|
||||
for tileVideoEndpoint in self.videoNodesOrder {
|
||||
for tileVideoEndpoint in self.videoOrder {
|
||||
if let _ = tileByVideoEndpoint[tileVideoEndpoint] {
|
||||
preList.append(tileVideoEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
if (tileByVideoEndpoint.count % 2) != 0, let last = preList.last, !self.wideVideoNodes.contains(last), let latestWide = latestWideVideo {
|
||||
self.videoNodesOrder.removeAll(where: { $0 == latestWide })
|
||||
self.videoNodesOrder.append(latestWide)
|
||||
self.videoOrder.removeAll(where: { $0 == latestWide })
|
||||
self.videoOrder.append(latestWide)
|
||||
}
|
||||
|
||||
for tileVideoEndpoint in self.videoNodesOrder {
|
||||
for tileVideoEndpoint in self.videoOrder {
|
||||
if let tileItem = tileByVideoEndpoint[tileVideoEndpoint] {
|
||||
tileItems.append(tileItem)
|
||||
if let fullscreenEntry = entryByPeerId[tileItem.peer.id] {
|
||||
|
|
@ -4352,13 +4386,16 @@ public final class VoiceChatController: ViewController {
|
|||
strongSelf.readyVideoDisposables.set((videoNode.ready
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self, weak videoNode] _ in
|
||||
if let strongSelf = self, let videoNode = videoNode {
|
||||
strongSelf.readyVideoNodes.insert(channel.endpointId)
|
||||
if videoNode.aspectRatio > 1.25 {
|
||||
strongSelf.wideVideoNodes.insert(channel.endpointId)
|
||||
Queue.mainQueue().after(0.1) {
|
||||
strongSelf.readyVideoNodes.insert(channel.endpointId)
|
||||
if videoNode.aspectRatio <= 0.77 {
|
||||
strongSelf.wideVideoNodes.insert(channel.endpointId)
|
||||
}
|
||||
strongSelf.updateMembers()
|
||||
}
|
||||
strongSelf.updateMembers()
|
||||
}
|
||||
}), forKey: channel.endpointId)
|
||||
strongSelf.videoNodes[channel.endpointId] = videoNode
|
||||
|
|
@ -4386,6 +4423,8 @@ public final class VoiceChatController: ViewController {
|
|||
for (videoEndpointId, _) in self.videoNodes {
|
||||
if !validSources.contains(videoEndpointId) {
|
||||
self.videoNodes[videoEndpointId] = nil
|
||||
self.videoOrder.removeAll(where: { $0 == videoEndpointId })
|
||||
self.readyVideoNodes.remove(videoEndpointId)
|
||||
self.readyVideoDisposables.set(nil, forKey: videoEndpointId)
|
||||
}
|
||||
}
|
||||
|
|
@ -4637,7 +4676,7 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
} else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) {
|
||||
if velocity.y > -1500.0 && !self.isFullscreen {
|
||||
if velocity.y > -2200.0 && !self.isFullscreen {
|
||||
DispatchQueue.main.async {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
|
|
@ -5057,20 +5096,20 @@ public final class VoiceChatController: ViewController {
|
|||
|
||||
self.updateDecorationsLayout(transition: .immediate)
|
||||
|
||||
var minimalVisiblePeerid: (PeerId, CGFloat)?
|
||||
var minimalVisiblePeerid: (PeerId, CGPoint)?
|
||||
var verticalItemNodes: [PeerId: ASDisplayNode] = [:]
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
|
||||
for tileNode in itemNode.tileNodes {
|
||||
let convertedFrame = tileNode.view.convert(tileNode.bounds, to: self.transitionContainerNode.view)
|
||||
if let item = tileNode.item {
|
||||
if let (_, y) = minimalVisiblePeerid {
|
||||
if convertedFrame.minY >= 0.0 && convertedFrame.minY < y {
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.minY)
|
||||
if let (_, point) = minimalVisiblePeerid {
|
||||
if convertedFrame.minY >= 0.0 && (convertedFrame.minY < point.y || (convertedFrame.minY == point.y && convertedFrame.minX < point.x)) {
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.origin)
|
||||
}
|
||||
} else {
|
||||
if convertedFrame.minY >= 0.0 {
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.minY)
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.origin)
|
||||
}
|
||||
}
|
||||
verticalItemNodes[item.peer.id] = tileNode
|
||||
|
|
@ -5078,13 +5117,13 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
} else if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item {
|
||||
let convertedFrame = itemNode.view.convert(itemNode.bounds, to: self.transitionContainerNode.view)
|
||||
if let (_, y) = minimalVisiblePeerid {
|
||||
if convertedFrame.minY >= 0.0 && convertedFrame.minY < y {
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.minY)
|
||||
if let (_, point) = minimalVisiblePeerid {
|
||||
if convertedFrame.minY >= 0.0 && convertedFrame.minY < point.y {
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.origin)
|
||||
}
|
||||
} else {
|
||||
if convertedFrame.minY >= 0.0 {
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.minY)
|
||||
minimalVisiblePeerid = (item.peer.id, convertedFrame.origin)
|
||||
}
|
||||
}
|
||||
verticalItemNodes[item.peer.id] = itemNode
|
||||
|
|
@ -5104,30 +5143,44 @@ public final class VoiceChatController: ViewController {
|
|||
|
||||
self.fullscreenListNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item, let otherItemNode = verticalItemNodes[item.peer.id] {
|
||||
itemNode.animateTransitionIn(from: otherItemNode, containerNode: self, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId)
|
||||
itemNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId)
|
||||
}
|
||||
}
|
||||
|
||||
self.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
Queue.mainQueue().after(0.2) {
|
||||
self?.transitionMaskBottomFillLayer.removeAllAnimations()
|
||||
}
|
||||
})
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
|
||||
self.updateDecorationsLayout(transition: transition)
|
||||
}
|
||||
}
|
||||
if false, let (peerId, _) = minimalVisiblePeerid {
|
||||
// if let (peerId, _) = minimalVisiblePeerid {
|
||||
let effectiveSpeakerPeerId = self.effectiveSpeaker?.0
|
||||
var index = 0
|
||||
for item in self.currentEntries {
|
||||
if case let .peer(entry, _) = item, entry.peer.id == peerId {
|
||||
for item in self.currentFullscreenEntries {
|
||||
if case let .peer(entry, _) = item, entry.peer.id == effectiveSpeakerPeerId {
|
||||
break
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
|
||||
let position: ListViewScrollPosition
|
||||
if index > self.currentFullscreenEntries.count - 3 {
|
||||
index = self.currentFullscreenEntries.count - 1
|
||||
position = .bottom(0.0)
|
||||
} else {
|
||||
position = .center(.bottom)
|
||||
}
|
||||
self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: position, animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
// } else {
|
||||
// completion()
|
||||
// }
|
||||
} else if case .fullscreen = previousDisplayMode, case .modal = self.displayMode {
|
||||
var minimalVisiblePeerid: (PeerId, CGFloat)?
|
||||
var fullscreenItemNodes: [PeerId: VoiceChatFullscreenParticipantItemNode] = [:]
|
||||
|
|
@ -5151,6 +5204,8 @@ public final class VoiceChatController: ViewController {
|
|||
let effectiveSpeakerPeerId = self.effectiveSpeaker?.0
|
||||
var targetTileNode: VoiceChatTileItemNode?
|
||||
|
||||
self.transitionContainerNode.addSubnode(self.mainStageNode)
|
||||
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
|
||||
for tileNode in itemNode.tileNodes {
|
||||
|
|
@ -5168,15 +5223,25 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
self.mainStageNode.animateTransitionOut(to: targetTileNode, transition: transition, completion: { [weak self] in
|
||||
self?.effectiveSpeaker = nil
|
||||
self?.mainStageNode.update(peer: nil, waitForFullSize: false)
|
||||
self?.fullscreenListNode.isHidden = true
|
||||
self?.mainStageContainerNode.isHidden = true
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.effectiveSpeaker = nil
|
||||
strongSelf.mainStageNode.update(peer: nil, waitForFullSize: false)
|
||||
strongSelf.fullscreenListNode.isHidden = true
|
||||
strongSelf.mainStageContainerNode.isHidden = true
|
||||
strongSelf.mainStageContainerNode.addSubnode(strongSelf.mainStageNode)
|
||||
})
|
||||
|
||||
self.mainStageBackgroundNode.alpha = 0.0
|
||||
self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
self.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
Queue.mainQueue().after(0.2) {
|
||||
self?.transitionMaskTopFillLayer.removeAllAnimations()
|
||||
}
|
||||
})
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
|
||||
self.updateDecorationsLayout(transition: transition)
|
||||
|
|
|
|||
|
|
@ -42,15 +42,20 @@ private let borderImage = generateImage(CGSize(width: tileSize.width, height: ti
|
|||
context.strokePath()
|
||||
})
|
||||
|
||||
private let fadeImage = generateImage(CGSize(width: 1.0, height: 30.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.7).cgColor] as CFArray
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
|
||||
private var fadeImage: UIImage? = {
|
||||
return generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let colorsArray = [fadeColor.withAlphaComponent(0.0).cgColor, fadeColor.cgColor] as CFArray
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}()
|
||||
|
||||
final class VoiceChatFullscreenParticipantItem: ListViewItem {
|
||||
enum Icon {
|
||||
|
|
@ -168,7 +173,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
private var raiseHandNode: VoiceChatRaiseHandNode?
|
||||
private var actionButtonNode: HighlightableButtonNode
|
||||
|
||||
private var audioLevelView: VoiceBlobView?
|
||||
var audioLevelView: VoiceBlobView?
|
||||
private let audioLevelDisposable = MetaDisposable()
|
||||
private var didSetupAudioLevel = false
|
||||
|
||||
|
|
@ -181,7 +186,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
private var wavesColor: UIColor?
|
||||
|
||||
let videoContainerNode: ASDisplayNode
|
||||
private let videoFadeNode: ASImageNode
|
||||
private let videoFadeNode: ASDisplayNode
|
||||
var videoNode: GroupVideoNode?
|
||||
private let videoReadyDisposable = MetaDisposable()
|
||||
private var videoReadyDelayed = false
|
||||
|
|
@ -225,12 +230,12 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
self.videoContainerNode = ASDisplayNode()
|
||||
self.videoContainerNode.clipsToBounds = true
|
||||
|
||||
self.videoFadeNode = ASImageNode()
|
||||
self.videoFadeNode = ASDisplayNode()
|
||||
self.videoFadeNode.displaysAsynchronously = false
|
||||
self.videoFadeNode.displayWithoutProcessing = true
|
||||
self.videoFadeNode.contentMode = .scaleToFill
|
||||
self.videoFadeNode.image = fadeImage
|
||||
self.videoContainerNode.addSubnode(videoFadeNode)
|
||||
if let image = fadeImage {
|
||||
self.videoFadeNode.backgroundColor = UIColor(patternImage: image)
|
||||
}
|
||||
self.videoContainerNode.addSubnode(self.videoFadeNode)
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
|
@ -310,7 +315,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
if let sourceNode = sourceNode as? VoiceChatTileItemNode {
|
||||
var startContainerPosition = sourceNode.view.convert(sourceNode.bounds, to: containerNode.view).center
|
||||
var animate = initialAnimate
|
||||
if startContainerPosition.y > containerNode.frame.height - 238.0 {
|
||||
if startContainerPosition.y < -tileHeight || startContainerPosition.y > containerNode.frame.height + tileHeight {
|
||||
animate = false
|
||||
}
|
||||
|
||||
|
|
@ -373,13 +378,14 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
} else if let sourceNode = sourceNode as? VoiceChatParticipantItemNode, let _ = sourceNode.item {
|
||||
var startContainerPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center
|
||||
var animate = true
|
||||
if startContainerPosition.y > containerNode.frame.height - 238.0 {
|
||||
if startContainerPosition.y > containerNode.frame.height {
|
||||
animate = false
|
||||
}
|
||||
startContainerPosition = startContainerPosition.offsetBy(dx: 0.0, dy: 9.0)
|
||||
|
||||
if animate {
|
||||
sourceNode.avatarNode.alpha = 0.0
|
||||
sourceNode.audioLevelView?.alpha = 0.0
|
||||
|
||||
let initialPosition = self.contextSourceNode.position
|
||||
let targetContainerPosition = self.contextSourceNode.view.convert(self.contextSourceNode.bounds, to: containerNode.view).center
|
||||
|
|
@ -388,8 +394,9 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
containerNode.addSubnode(self.contextSourceNode)
|
||||
|
||||
self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: duration, timingFunction: timingFunction, completion: { [weak self, weak sourceNode] _ in
|
||||
if let strongSelf = self {
|
||||
sourceNode?.avatarNode.alpha = 1.0
|
||||
if let strongSelf = self, let sourceNode = sourceNode {
|
||||
sourceNode.avatarNode.alpha = 1.0
|
||||
sourceNode.audioLevelView?.alpha = 1.0
|
||||
strongSelf.contextSourceNode.position = initialPosition
|
||||
strongSelf.containerNode.addSubnode(strongSelf.contextSourceNode)
|
||||
}
|
||||
|
|
@ -668,7 +675,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
audioLevelView.updateLevel(CGFloat(value))
|
||||
|
||||
let avatarScale: CGFloat
|
||||
if value > 0.0 {
|
||||
if value > 0.02 {
|
||||
audioLevelView.startAnimating()
|
||||
avatarScale = 1.03 + level * 0.13
|
||||
if let wavesColor = strongSelf.wavesColor {
|
||||
|
|
@ -683,7 +690,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
avatarScale = 1.0
|
||||
if strongSelf.silenceTimer == nil {
|
||||
let silenceTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
||||
self?.audioLevelView?.stopAnimating(duration: 0.5)
|
||||
self?.audioLevelView?.stopAnimating(duration: 0.75)
|
||||
self?.silenceTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.silenceTimer = silenceTimer
|
||||
|
|
@ -810,7 +817,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
let videoContainerScale = tileSize.width / videoSize.width
|
||||
|
||||
if !strongSelf.isExtracted && !strongSelf.animatingExtraction {
|
||||
strongSelf.videoFadeNode.frame = CGRect(x: 0.0, y: videoSize.height - 75.0, width: videoSize.width, height: 75.0)
|
||||
strongSelf.videoFadeNode.frame = CGRect(x: 0.0, y: videoSize.height - fadeHeight, width: videoSize.width, height: fadeHeight)
|
||||
strongSelf.videoContainerNode.bounds = CGRect(origin: CGPoint(), size: videoSize)
|
||||
|
||||
if let videoNode = strongSelf.videoNode {
|
||||
|
|
|
|||
|
|
@ -19,19 +19,23 @@ import AudioBlob
|
|||
|
||||
private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white)
|
||||
private let backgroundCornerRadius: CGFloat = 11.0
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
|
||||
final class VoiceChatMainStageNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let call: PresentationGroupCall
|
||||
private var currentPeer: (PeerId, String?)?
|
||||
private var currentPeerEntry: VoiceChatPeerEntry?
|
||||
|
||||
var callState: PresentationGroupCallState?
|
||||
|
||||
private var currentVideoNode: GroupVideoNode?
|
||||
private var candidateVideoNode: GroupVideoNode?
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topFadeNode: ASImageNode
|
||||
private let bottomFadeNode: ASImageNode
|
||||
private let topFadeNode: ASDisplayNode
|
||||
private let bottomFadeNode: ASDisplayNode
|
||||
private let bottomFillNode: ASDisplayNode
|
||||
private let headerNode: ASDisplayNode
|
||||
private let backButtonNode: HighlightableButtonNode
|
||||
private let backButtonArrowNode: ASImageNode
|
||||
|
|
@ -42,10 +46,12 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
private let audioLevelDisposable = MetaDisposable()
|
||||
private let speakingPeerDisposable = MetaDisposable()
|
||||
private let speakingAudioLevelDisposable = MetaDisposable()
|
||||
private var avatarNode: AvatarNode
|
||||
private var avatarNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let microphoneNode: VoiceChatMicrophoneNode
|
||||
|
||||
private let avatarDisposable = MetaDisposable()
|
||||
|
||||
private let speakingContainerNode: ASDisplayNode
|
||||
private var speakingEffectView: UIVisualEffectView?
|
||||
private let speakingAvatarNode: AvatarNode
|
||||
|
|
@ -70,34 +76,37 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
self.backgroundNode.alpha = 0.0
|
||||
self.backgroundNode.backgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||
|
||||
self.topFadeNode = ASImageNode()
|
||||
self.topFadeNode = ASDisplayNode()
|
||||
self.topFadeNode.alpha = 0.0
|
||||
self.topFadeNode.displaysAsynchronously = false
|
||||
self.topFadeNode.displayWithoutProcessing = true
|
||||
self.topFadeNode.contentMode = .scaleToFill
|
||||
self.topFadeNode.image = generateImage(CGSize(width: 1.0, height: 50.0), rotatedContext: { size, context in
|
||||
if let image = generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.7).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor] as CFArray
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorsArray = [fadeColor.cgColor, fadeColor.withAlphaComponent(0.0).cgColor] as CFArray
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}) {
|
||||
self.topFadeNode.backgroundColor = UIColor(patternImage: image)
|
||||
}
|
||||
|
||||
self.bottomFadeNode = ASImageNode()
|
||||
self.bottomFadeNode = ASDisplayNode()
|
||||
self.bottomFadeNode.displaysAsynchronously = false
|
||||
self.bottomFadeNode.displayWithoutProcessing = true
|
||||
self.bottomFadeNode.contentMode = .scaleToFill
|
||||
self.bottomFadeNode.image = generateImage(CGSize(width: 1.0, height: 50.0), rotatedContext: { size, context in
|
||||
if let image = generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.7).cgColor] as CFArray
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorsArray = [fadeColor.withAlphaComponent(0.0).cgColor, fadeColor.cgColor] as CFArray
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}) {
|
||||
self.bottomFadeNode.backgroundColor = UIColor(patternImage: image)
|
||||
}
|
||||
|
||||
self.bottomFillNode = ASDisplayNode()
|
||||
self.bottomFillNode.backgroundColor = fadeColor
|
||||
|
||||
self.headerNode = ASDisplayNode()
|
||||
self.headerNode.alpha = 0.0
|
||||
|
|
@ -117,7 +126,8 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
self.pinButtonTitleNode.attributedText = NSAttributedString(string: "Unpin", font: Font.regular(17.0), textColor: .white)
|
||||
self.pinButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0))
|
||||
self.avatarNode = ASImageNode()
|
||||
self.avatarNode.displaysAsynchronously = false
|
||||
self.avatarNode.isHidden = true
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
|
@ -144,6 +154,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.topFadeNode)
|
||||
self.addSubnode(self.bottomFadeNode)
|
||||
self.addSubnode(self.bottomFillNode)
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.microphoneNode)
|
||||
|
|
@ -194,6 +205,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
deinit {
|
||||
self.avatarDisposable.dispose()
|
||||
self.videoReadyDisposable.dispose()
|
||||
self.audioLevelDisposable.dispose()
|
||||
self.speakingPeerDisposable.dispose()
|
||||
|
|
@ -233,7 +245,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 1.0)
|
||||
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 1.0)
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: 1.0)
|
||||
|
|
@ -271,7 +283,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
|
||||
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 0.0)
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: 0.0)
|
||||
|
|
@ -326,9 +338,33 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
|
||||
var wavesColor = UIColor(rgb: 0x34c759)
|
||||
if let getAudioLevel = self.getAudioLevel, let peerId = speakingPeerId {
|
||||
self.speakingAudioLevelView?.removeFromSuperview()
|
||||
let wavesColor = UIColor(rgb: 0x34c759)
|
||||
if let speakingAudioLevelView = self.speakingAudioLevelView {
|
||||
speakingAudioLevelView.removeFromSuperview()
|
||||
self.speakingAudioLevelView = nil
|
||||
}
|
||||
|
||||
self.speakingPeerDisposable.set((self.context.account.postbox.loadedPeerWithId(peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.speakingAvatarNode.setPeer(context: strongSelf.context, theme: presentationData.theme, peer: peer)
|
||||
strongSelf.speakingTitleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(14.0), textColor: .white)
|
||||
|
||||
strongSelf.speakingContainerNode.alpha = 0.0
|
||||
|
||||
if let (size, sideInset, bottomInset, isLandscape) = strongSelf.validLayout {
|
||||
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate)
|
||||
}
|
||||
|
||||
strongSelf.speakingContainerNode.alpha = 1.0
|
||||
strongSelf.speakingContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
strongSelf.speakingContainerNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}))
|
||||
|
||||
let blobFrame = self.speakingAvatarNode.frame.insetBy(dx: -14.0, dy: -14.0)
|
||||
self.speakingAudioLevelDisposable.set((getAudioLevel(peerId)
|
||||
|
|
@ -373,10 +409,19 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
}))
|
||||
} else {
|
||||
self.speakingPeerDisposable.set(nil)
|
||||
self.speakingAudioLevelDisposable.set(nil)
|
||||
|
||||
if let audioLevelView = self.audioLevelView {
|
||||
audioLevelView.removeFromSuperview()
|
||||
self.audioLevelView = nil
|
||||
let audioLevelView = self.audioLevelView
|
||||
self.audioLevelView = nil
|
||||
|
||||
if !self.speakingContainerNode.alpha.isZero {
|
||||
self.speakingContainerNode.alpha = 0.0
|
||||
self.speakingContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in
|
||||
audioLevelView?.removeFromSuperview()
|
||||
})
|
||||
self.speakingContainerNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
} else {
|
||||
audioLevelView?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -387,16 +432,12 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
if !arePeersEqual(previousPeerEntry?.peer, peerEntry.peer) {
|
||||
let peer = peerEntry.peer
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if previousPeerEntry?.peer.id == peerEntry.peer.id {
|
||||
self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: peer)
|
||||
} else {
|
||||
let previousAvatarNode = self.avatarNode
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0))
|
||||
self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: peer, synchronousLoad: true)
|
||||
self.avatarNode.frame = previousAvatarNode.frame
|
||||
previousAvatarNode.supernode?.insertSubnode(self.avatarNode, aboveSubnode: previousAvatarNode)
|
||||
previousAvatarNode.removeFromSupernode()
|
||||
}
|
||||
self.avatarDisposable.set((peerAvatarCompleteImage(account: self.context.account, peer: peer, size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
if let strongSelf = self {
|
||||
strongSelf.avatarNode.image = image
|
||||
}
|
||||
}))
|
||||
self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: .white)
|
||||
if let (size, sideInset, bottomInset, isLandscape) = self.validLayout {
|
||||
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate)
|
||||
|
|
@ -408,7 +449,10 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
|
||||
var wavesColor = UIColor(rgb: 0x34c759)
|
||||
if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id {
|
||||
self.audioLevelView?.removeFromSuperview()
|
||||
if let audioLevelView = self.audioLevelView {
|
||||
self.audioLevelView = nil
|
||||
audioLevelView.removeFromSuperview()
|
||||
}
|
||||
|
||||
let blobFrame = self.avatarNode.frame.insetBy(dx: -60.0, dy: -60.0)
|
||||
self.audioLevelDisposable.set((getAudioLevel(peerEntry.peer.id)
|
||||
|
|
@ -452,7 +496,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
avatarScale = 1.0
|
||||
if strongSelf.silenceTimer == nil {
|
||||
let silenceTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
||||
self?.audioLevelView?.stopAnimating(duration: 0.5)
|
||||
self?.audioLevelView?.stopAnimating(duration: 0.75)
|
||||
self?.silenceTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.silenceTimer = silenceTimer
|
||||
|
|
@ -612,11 +656,12 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
|
||||
transition.updateFrame(node: self.microphoneNode, frame: CGRect(origin: CGPoint(x: sideInset + 7.0, y: size.height - bottomInset - animationSize.height - 6.0), size: animationSize))
|
||||
|
||||
var fadeHeight: CGFloat = 50.0
|
||||
if size.height != 180.0 && size.width < size.height {
|
||||
fadeHeight = 140.0
|
||||
var totalFadeHeight: CGFloat = fadeHeight
|
||||
if size.height != tileHeight && size.width < size.height {
|
||||
totalFadeHeight += bottomInset
|
||||
}
|
||||
transition.updateFrame(node: self.bottomFadeNode, frame: CGRect(x: 0.0, y: size.height - fadeHeight, width: size.width, height: fadeHeight))
|
||||
transition.updateFrame(node: self.bottomFadeNode, frame: CGRect(x: 0.0, y: size.height - totalFadeHeight, width: size.width, height: fadeHeight))
|
||||
transition.updateFrame(node: self.bottomFillNode, frame: CGRect(x: 0.0, y: size.height - totalFadeHeight + fadeHeight, width: size.width, height: max(0.0, totalFadeHeight - fadeHeight)))
|
||||
transition.updateFrame(node: self.topFadeNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: 50.0))
|
||||
|
||||
let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0))
|
||||
|
|
@ -635,4 +680,11 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
|
||||
transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 64.0)))
|
||||
}
|
||||
|
||||
func flipVideoIfNeeded() {
|
||||
guard self.currentPeer?.0 == self.callState?.myPeerId else {
|
||||
return
|
||||
}
|
||||
self.currentVideoNode?.flip(withBackground: false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ class VoiceChatParticipantStatusNode: ASDisplayNode {
|
|||
class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightContainerNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
let contextSourceNode: ContextExtractedContentContainingNode
|
||||
|
|
@ -275,7 +276,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
private var raiseHandNode: VoiceChatRaiseHandNode?
|
||||
private var actionButtonNode: HighlightableButtonNode
|
||||
|
||||
private var audioLevelView: VoiceBlobView?
|
||||
var audioLevelView: VoiceBlobView?
|
||||
private let audioLevelDisposable = MetaDisposable()
|
||||
private var didSetupAudioLevel = false
|
||||
|
||||
|
|
@ -337,13 +338,17 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
self.actionContainerNode = ASDisplayNode()
|
||||
self.actionButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.highlightContainerNode = ASDisplayNode()
|
||||
self.highlightContainerNode.clipsToBounds = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.isAccessibilityElement = true
|
||||
|
||||
self.highlightContainerNode.addSubnode(self.highlightedBackgroundNode)
|
||||
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
|
|
@ -516,6 +521,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.height / 2.0, width: targetRect.width, height: targetRect.height)
|
||||
avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.height / 2.0), size: CGSize())
|
||||
avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0)
|
||||
avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0)
|
||||
|
||||
avatarListContainerNode.addSubnode(avatarListNode)
|
||||
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
|
||||
|
|
@ -650,6 +656,14 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
self.silenceTimer?.invalidate()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
self.highlightContainerNode.layer.cornerCurve = .continuous
|
||||
}
|
||||
}
|
||||
|
||||
override func selected() {
|
||||
super.selected()
|
||||
self.layoutParams?.0.action?(self.contextSourceNode)
|
||||
|
|
@ -673,7 +687,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
if animate {
|
||||
sourceNode.avatarNode.alpha = 0.0
|
||||
|
||||
sourceNode.audioLevelView?.alpha = 0.0
|
||||
|
||||
let initialAvatarPosition = self.avatarNode.position
|
||||
let initialBackgroundPosition = sourceNode.backgroundImageNode.position
|
||||
let initialContentPosition = sourceNode.contentWrapperNode.position
|
||||
|
|
@ -706,11 +721,19 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
sourceNode.contentWrapperNode.layer.animatePosition(from: startContainerContentPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak sourceNode] _ in
|
||||
if let sourceNode = sourceNode {
|
||||
sourceNode.avatarNode.alpha = 1.0
|
||||
sourceNode.audioLevelView?.alpha = 1.0
|
||||
sourceNode.contentWrapperNode.position = initialContentPosition
|
||||
sourceNode.offsetContainerNode.insertSubnode(sourceNode.contentWrapperNode, aboveSubnode: sourceNode.videoContainerNode)
|
||||
}
|
||||
})
|
||||
|
||||
if let audioLevelView = self.audioLevelView {
|
||||
audioLevelView.center = targetContainerAvatarPosition
|
||||
containerNode.view.addSubview(audioLevelView)
|
||||
|
||||
audioLevelView.layer.animateScale(from: 1.25, to: 1.0, duration: duration, timingFunction: timingFunction)
|
||||
audioLevelView.layer.animatePosition(from: startContainerAvatarPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
}
|
||||
self.avatarNode.position = targetContainerAvatarPosition
|
||||
containerNode.addSubnode(self.avatarNode)
|
||||
|
||||
|
|
@ -719,6 +742,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
if let strongSelf = self {
|
||||
strongSelf.avatarNode.position = initialAvatarPosition
|
||||
strongSelf.offsetContainerNode.addSubnode(strongSelf.avatarNode)
|
||||
if let audioLevelView = strongSelf.audioLevelView {
|
||||
audioLevelView.layer.removeAllAnimations()
|
||||
audioLevelView.center = initialAvatarPosition
|
||||
strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -1031,7 +1059,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
audioLevelView.updateLevel(CGFloat(value))
|
||||
|
||||
let avatarScale: CGFloat
|
||||
if value > 0.0 {
|
||||
if value > 0.02 {
|
||||
audioLevelView.startAnimating()
|
||||
avatarScale = 1.03 + level * 0.13
|
||||
if let wavesColor = strongSelf.wavesColor {
|
||||
|
|
@ -1046,7 +1074,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
avatarScale = 1.0
|
||||
if strongSelf.silenceTimer == nil {
|
||||
let silenceTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
||||
self?.audioLevelView?.stopAnimating(duration: 0.5)
|
||||
self?.audioLevelView?.stopAnimating(duration: 0.75)
|
||||
self?.silenceTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.silenceTimer = silenceTimer
|
||||
|
|
@ -1072,7 +1100,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad, storeUnrounded: true)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.highlightContainerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: -UIScreenPixel), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel + 11.0))
|
||||
|
||||
strongSelf.highlightContainerNode.cornerRadius = first ? 11.0 : 0.0
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
var hadMicrophoneNode = false
|
||||
var hadRaiseHandNode = false
|
||||
|
|
@ -1185,8 +1217,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
var isHighlighted = false
|
||||
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
if self.isHighlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.highlightContainerNode.alpha = 1.0
|
||||
if self.highlightContainerNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
|
|
@ -1194,24 +1226,24 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
anchorNode = self.topStripeNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
self.insertSubnode(self.highlightContainerNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.highlightContainerNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if self.highlightContainerNode.supernode != nil {
|
||||
if transition.isAnimated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
self.highlightContainerNode.layer.animateAlpha(from: self.highlightContainerNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
strongSelf.highlightContainerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
self.highlightContainerNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
self.highlightContainerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
|||
self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode)
|
||||
|
||||
if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
self.videoFadeNode.image = sourceNode.fadeNode.image
|
||||
self.videoFadeNode.image = tileFadeImage
|
||||
self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height)
|
||||
|
||||
self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode)
|
||||
|
|
@ -260,6 +260,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
|||
self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width)
|
||||
self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize())
|
||||
self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0)
|
||||
self.avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0)
|
||||
|
||||
self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: self.customNode, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate)
|
||||
|
||||
|
|
@ -339,6 +340,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
|||
self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width)
|
||||
self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize())
|
||||
self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0)
|
||||
self.avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0)
|
||||
|
||||
self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: nil, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate)
|
||||
|
||||
|
|
@ -409,6 +411,9 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
|||
self.avatarListNode.stripContainerNode.alpha = 0.0
|
||||
self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
self.avatarListNode.shadowNode.alpha = 0.0
|
||||
self.avatarListNode.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
self.infoNode.alpha = 0.0
|
||||
self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
} else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode {
|
||||
|
|
@ -461,6 +466,9 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
|||
|
||||
self.avatarListNode.stripContainerNode.alpha = 0.0
|
||||
self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
self.avatarListNode.shadowNode.alpha = 0.0
|
||||
self.avatarListNode.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
self.infoNode.alpha = 0.0
|
||||
self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import SwiftSignalKit
|
|||
import AccountContext
|
||||
|
||||
private let tileSpacing: CGFloat = 4.0
|
||||
private let tileHeight: CGFloat = 180.0
|
||||
let tileHeight: CGFloat = 180.0
|
||||
|
||||
final class VoiceChatTileGridNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
|
|
@ -31,6 +31,11 @@ final class VoiceChatTileGridNode: ASDisplayNode {
|
|||
let halfWidth = floorToScreenPixels((size.width - tileSpacing) / 2.0)
|
||||
let lastItemIsWide = items.count % 2 != 0
|
||||
|
||||
let isFirstTime = self.isFirstTime
|
||||
if isFirstTime {
|
||||
self.isFirstTime = false
|
||||
}
|
||||
|
||||
for i in 0 ..< self.items.count {
|
||||
let item = self.items[i]
|
||||
let isLast = i == self.items.count - 1
|
||||
|
|
@ -60,10 +65,8 @@ final class VoiceChatTileGridNode: ASDisplayNode {
|
|||
if let itemNode = itemNode {
|
||||
if wasAdded {
|
||||
itemNode.frame = itemFrame
|
||||
if self.isFirstTime {
|
||||
self.isFirstTime = false
|
||||
} else {
|
||||
itemNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2)
|
||||
if !isFirstTime {
|
||||
itemNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.3)
|
||||
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -80,7 +83,10 @@ final class VoiceChatTileGridNode: ASDisplayNode {
|
|||
}
|
||||
for id in removeIds {
|
||||
if let itemNode = self.itemNodes.removeValue(forKey: id) {
|
||||
itemNode.removeFromSupernode()
|
||||
itemNode.layer.animateScale(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in
|
||||
itemNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +208,7 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode {
|
|||
strongSelf.tileGridNode = tileGridNode
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = currentItem == nil ? .immediate : .animated(duration: 0.4, curve: .spring)
|
||||
let transition: ContainedViewLayoutTransition = currentItem == nil ? .immediate : .animated(duration: 0.3, curve: .easeInOut)
|
||||
let tileGridSize = tileGridNode.update(size: CGSize(width: params.width - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), items: item.tiles, transition: transition)
|
||||
if currentItem == nil {
|
||||
tileGridNode.frame = CGRect(x: params.leftInset, y: 0.0, width: tileGridSize.width, height: 0.0)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ private let borderImage = generateImage(CGSize(width: 24.0, height: 24.0), rotat
|
|||
context.strokePath()
|
||||
})
|
||||
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
|
||||
final class VoiceChatTileItem: Equatable {
|
||||
enum Icon: Equatable {
|
||||
case none
|
||||
|
|
@ -80,13 +78,16 @@ final class VoiceChatTileItem: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
private var fadeImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 1.0, height: fadeHeight), rotatedContext: { size, context in
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
|
||||
var tileFadeImage: UIImage? = {
|
||||
return generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.7).cgColor] as CFArray
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorsArray = [fadeColor.withAlphaComponent(0.0).cgColor, fadeColor.cgColor] as CFArray
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
|
|
@ -102,7 +103,7 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||
var videoContainerNode: ASDisplayNode
|
||||
var videoNode: GroupVideoNode?
|
||||
let infoNode: ASDisplayNode
|
||||
let fadeNode: ASImageNode
|
||||
let fadeNode: ASDisplayNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let iconNode: ASImageNode
|
||||
private var animationNode: VoiceChatMicrophoneNode?
|
||||
|
|
@ -139,11 +140,11 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||
|
||||
self.infoNode = ASDisplayNode()
|
||||
|
||||
self.fadeNode = ASImageNode()
|
||||
self.fadeNode = ASDisplayNode()
|
||||
self.fadeNode.displaysAsynchronously = false
|
||||
self.fadeNode.displayWithoutProcessing = true
|
||||
self.fadeNode.contentMode = .scaleToFill
|
||||
self.fadeNode.image = fadeImage
|
||||
if let image = tileFadeImage {
|
||||
self.fadeNode.backgroundColor = UIColor(patternImage: image)
|
||||
}
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.statusNode = VoiceChatParticipantStatusNode()
|
||||
|
|
@ -338,11 +339,12 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
self.contentNode.frame = bounds
|
||||
self.containerNode.frame = bounds
|
||||
self.contextSourceNode.frame = bounds
|
||||
self.contextSourceNode.contentNode.frame = bounds
|
||||
|
||||
transition.updateFrame(node: self.contentNode, frame: bounds)
|
||||
|
||||
let extractedWidth = availableWidth
|
||||
let makeStatusLayout = self.statusNode.asyncLayout()
|
||||
let (statusLayout, _) = makeStatusLayout(CGSize(width: availableWidth - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, true)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue