Video Chat Improvements

This commit is contained in:
Ilya Laktyushin 2021-05-21 16:02:08 +04:00
parent 31067a651a
commit 659ea466c9
19 changed files with 2815 additions and 2706 deletions

View file

@ -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";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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