Video Chat Improvements

This commit is contained in:
Ilya Laktyushin 2021-05-19 21:05:28 +04:00
parent f680c5e11d
commit 71818a565e
5 changed files with 767 additions and 630 deletions

View File

@ -283,7 +283,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.0)
transition.updateAlpha(node: self.titleLabel, alpha: 0.0)
transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0)
transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint(x: 0.0, y: -40.0))
transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint(x: 0.0, y: -50.0))
} else {
transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.0)
transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.0)

View File

@ -403,8 +403,10 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
let buttonOffset: CGFloat = 120.0
var buttonOffset: CGFloat = 60.0
if let _ = self.broadcastPickerView {
buttonOffset *= 2.0
}
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
let titleHeight: CGFloat = 54.0
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
@ -457,6 +459,8 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: screenButtonHeight))
if let broadcastPickerView = self.broadcastPickerView {
broadcastPickerView.frame = CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width + 1000.0, height: screenButtonHeight)
} else {
self.screenButton.isHidden = true
}
let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)

View File

@ -43,7 +43,7 @@ let bottomAreaHeight: CGFloat = 206.0
private let fullscreenBottomAreaHeight: CGFloat = 80.0
private let bottomGradientHeight: CGFloat = 70.0
private func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
if !top && !bottom {
return nil
}
@ -82,6 +82,123 @@ private func decorationBottomGradientImage(dark: Bool) -> UIImage? {
})
}
struct VoiceChatPeerEntry: Comparable, Identifiable {
enum State {
case listening
case speaking
case invited
case raisedHand
}
var peer: Peer
var about: String?
var isMyPeer: Bool
var videoEndpointId: String?
var presentationEndpointId: String?
var activityTimestamp: Int32
var state: State
var muteState: GroupCallParticipantsContext.Participant.MuteState?
var canManageCall: Bool
var volume: Int32?
var raisedHand: Bool
var displayRaisedHandStatus: Bool
var active: Bool
var isLandscape: Bool
var effectiveVideoEndpointId: String? {
return self.presentationEndpointId ?? self.videoEndpointId
}
init(
peer: Peer,
about: String?,
isMyPeer: Bool,
videoEndpointId: String?,
presentationEndpointId: String?,
activityTimestamp: Int32,
state: State,
muteState: GroupCallParticipantsContext.Participant.MuteState?,
canManageCall: Bool,
volume: Int32?,
raisedHand: Bool,
displayRaisedHandStatus: Bool,
active: Bool,
isLandscape: Bool
) {
self.peer = peer
self.about = about
self.isMyPeer = isMyPeer
self.videoEndpointId = videoEndpointId
self.presentationEndpointId = presentationEndpointId
self.activityTimestamp = activityTimestamp
self.state = state
self.muteState = muteState
self.canManageCall = canManageCall
self.volume = volume
self.raisedHand = raisedHand
self.displayRaisedHandStatus = displayRaisedHandStatus
self.active = active
self.isLandscape = isLandscape
}
var stableId: PeerId {
return self.peer.id
}
static func ==(lhs: VoiceChatPeerEntry, rhs: VoiceChatPeerEntry) -> Bool {
if !lhs.peer.isEqual(rhs.peer) {
return false
}
if lhs.about != rhs.about {
return false
}
if lhs.isMyPeer != rhs.isMyPeer {
return false
}
if lhs.videoEndpointId != rhs.videoEndpointId {
return false
}
if lhs.presentationEndpointId != rhs.presentationEndpointId {
return false
}
if lhs.activityTimestamp != rhs.activityTimestamp {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.muteState != rhs.muteState {
return false
}
if lhs.canManageCall != rhs.canManageCall {
return false
}
if lhs.volume != rhs.volume {
return false
}
if lhs.raisedHand != rhs.raisedHand {
return false
}
if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus {
return false
}
if lhs.active != rhs.active {
return false
}
if lhs.isLandscape != rhs.isLandscape {
return false
}
return true
}
static func <(lhs: VoiceChatPeerEntry, rhs: VoiceChatPeerEntry) -> Bool {
if lhs.activityTimestamp != rhs.activityTimestamp {
return lhs.activityTimestamp > rhs.activityTimestamp
}
return lhs.peer.id < rhs.peer.id
}
}
public final class VoiceChatController: ViewController {
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private struct ListTransition {
@ -101,7 +218,7 @@ public final class VoiceChatController: ViewController {
let switchToPeer: (PeerId, String?, Bool) -> Void
let togglePeerVideo: (PeerId) -> Void
let openInvite: () -> Void
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
let peerContextAction: (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?) -> Void
let getPeerVideo: (String, Bool) -> GroupVideoNode?
var isExpanded: Bool = false
@ -114,7 +231,7 @@ public final class VoiceChatController: ViewController {
switchToPeer: @escaping (PeerId, String?, Bool) -> Void,
togglePeerVideo: @escaping (PeerId) -> Void,
openInvite: @escaping () -> Void,
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void,
peerContextAction: @escaping (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?) -> Void,
getPeerVideo: @escaping (String, Bool) -> GroupVideoNode?
) {
self.updateIsMuted = updateIsMuted
@ -162,123 +279,6 @@ public final class VoiceChatController: ViewController {
}
}
fileprivate struct PeerEntry: Comparable, Identifiable {
enum State {
case listening
case speaking
case invited
case raisedHand
}
var peer: Peer
var about: String?
var isMyPeer: Bool
var videoEndpointId: String?
var presentationEndpointId: String?
var activityTimestamp: Int32
var state: State
var muteState: GroupCallParticipantsContext.Participant.MuteState?
var canManageCall: Bool
var volume: Int32?
var raisedHand: Bool
var displayRaisedHandStatus: Bool
var active: Bool
var isLandscape: Bool
var effectiveVideoEndpointId: String? {
return self.presentationEndpointId ?? self.videoEndpointId
}
init(
peer: Peer,
about: String?,
isMyPeer: Bool,
videoEndpointId: String?,
presentationEndpointId: String?,
activityTimestamp: Int32,
state: State,
muteState: GroupCallParticipantsContext.Participant.MuteState?,
canManageCall: Bool,
volume: Int32?,
raisedHand: Bool,
displayRaisedHandStatus: Bool,
active: Bool,
isLandscape: Bool
) {
self.peer = peer
self.about = about
self.isMyPeer = isMyPeer
self.videoEndpointId = videoEndpointId
self.presentationEndpointId = presentationEndpointId
self.activityTimestamp = activityTimestamp
self.state = state
self.muteState = muteState
self.canManageCall = canManageCall
self.volume = volume
self.raisedHand = raisedHand
self.displayRaisedHandStatus = displayRaisedHandStatus
self.active = active
self.isLandscape = isLandscape
}
var stableId: PeerId {
return self.peer.id
}
static func ==(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
if !lhs.peer.isEqual(rhs.peer) {
return false
}
if lhs.about != rhs.about {
return false
}
if lhs.isMyPeer != rhs.isMyPeer {
return false
}
if lhs.videoEndpointId != rhs.videoEndpointId {
return false
}
if lhs.presentationEndpointId != rhs.presentationEndpointId {
return false
}
if lhs.activityTimestamp != rhs.activityTimestamp {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.muteState != rhs.muteState {
return false
}
if lhs.canManageCall != rhs.canManageCall {
return false
}
if lhs.volume != rhs.volume {
return false
}
if lhs.raisedHand != rhs.raisedHand {
return false
}
if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus {
return false
}
if lhs.active != rhs.active {
return false
}
if lhs.isLandscape != rhs.isLandscape {
return false
}
return true
}
static func <(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
if lhs.activityTimestamp != rhs.activityTimestamp {
return lhs.activityTimestamp > rhs.activityTimestamp
}
return lhs.peer.id < rhs.peer.id
}
}
private enum EntryId: Hashable {
case tiles
case invite
@ -318,7 +318,7 @@ public final class VoiceChatController: ViewController {
private enum ListEntry: Comparable, Identifiable {
case tiles([VoiceChatTileItem])
case invite(PresentationTheme, PresentationStrings, String, Bool)
case peer(PeerEntry)
case peer(VoiceChatPeerEntry)
var stableId: EntryId {
switch self {
@ -688,6 +688,7 @@ public final class VoiceChatController: ViewController {
private let optionsButton: VoiceChatHeaderButton
private let closeButton: VoiceChatHeaderButton
private let topCornersNode: ASImageNode
private let videoBottomCornersNode: ASImageNode
private let bottomPanelCoverNode: ASDisplayNode
fileprivate let bottomPanelNode: ASDisplayNode
private let bottomGradientNode: ASImageNode
@ -700,7 +701,7 @@ public final class VoiceChatController: ViewController {
fileprivate let actionButton: VoiceChatActionButton
private let leftBorderNode: ASDisplayNode
private let rightBorderNode: ASDisplayNode
private let mainStageNode: VoiceChatMainStageContainerNode
private let mainStageNode: VoiceChatMainStageNode
private let mainStageContainerNode: ASDisplayNode
private let transitionContainerNode: ASDisplayNode
@ -935,6 +936,12 @@ public final class VoiceChatController: ViewController {
self.bottomCornersNode.image = decorationCornersImage(top: false, bottom: true, dark: false)
self.bottomCornersNode.isUserInteractionEnabled = false
self.videoBottomCornersNode = ASImageNode()
self.videoBottomCornersNode.displaysAsynchronously = false
self.videoBottomCornersNode.displayWithoutProcessing = true
self.videoBottomCornersNode.image = decorationCornersImage(top: false, bottom: true, dark: false)
self.videoBottomCornersNode.isUserInteractionEnabled = false
self.audioButton = CallControllerButtonItemNode()
self.cameraButton = CallControllerButtonItemNode()
self.switchCameraButton = CallControllerButtonItemNode()
@ -962,7 +969,7 @@ public final class VoiceChatController: ViewController {
self.rightBorderNode.isUserInteractionEnabled = false
self.rightBorderNode.clipsToBounds = false
self.mainStageNode = VoiceChatMainStageContainerNode(context: self.context, call: self.call)
self.mainStageNode = VoiceChatMainStageNode(context: self.context, call: self.call)
self.mainStageContainerNode = ASDisplayNode()
self.mainStageContainerNode.clipsToBounds = true
@ -1320,7 +1327,7 @@ public final class VoiceChatController: ViewController {
let muteStatePromise = Promise<GroupCallParticipantsContext.Participant.MuteState?>(entry.muteState)
let itemsForEntry: (PeerEntry, GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { entry, muteState in
let itemsForEntry: (VoiceChatPeerEntry, GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { entry, muteState in
var items: [ContextMenuItem] = []
var hasVolumeSlider = false
@ -2070,12 +2077,14 @@ public final class VoiceChatController: ViewController {
})))
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_VideoPreviewShareScreen, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
if #available(iOS 12.0, *) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_VideoPreviewShareScreen, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
})))
})))
}
if canManageCall {
if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp {
@ -3272,6 +3281,20 @@ public final class VoiceChatController: ViewController {
}
self.bottomCornersNode.image = decorationCornersImage(top: false, bottom: true, dark: isFullscreen)
if let gridNode = gridNode {
if let snapshotView = gridNode.cornersNode.view.snapshotContentTree() {
snapshotView.frame = gridNode.cornersNode.bounds
gridNode.cornersNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
gridNode.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: isFullscreen)
gridNode.supernode?.addSubnode(gridNode)
}
UIView.transition(with: self.bottomGradientNode.view, duration: 0.3, options: [.transitionCrossDissolve, .curveLinear]) {
self.bottomGradientNode.image = decorationBottomGradientImage(dark: isFullscreen)
} completion: { _ in
@ -4011,7 +4034,7 @@ public final class VoiceChatController: ViewController {
}
processedPeerIds.insert(member.peer.id)
let memberState: PeerEntry.State
let memberState: VoiceChatPeerEntry.State
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
if member.hasRaiseHand && !(member.muteState?.canUnmute ?? false) {
memberState = .raisedHand
@ -4066,7 +4089,7 @@ public final class VoiceChatController: ViewController {
peerIdToEndpointId[member.peer.id] = anyEndpointId
}
let peerEntry = PeerEntry(
let peerEntry = VoiceChatPeerEntry(
peer: memberPeer,
about: member.about,
isMyPeer: self.callState?.myPeerId == member.peer.id,
@ -4165,7 +4188,7 @@ public final class VoiceChatController: ViewController {
}
processedPeerIds.insert(peer.id)
entries.append(.peer(PeerEntry(
entries.append(.peer(VoiceChatPeerEntry(
peer: peer,
about: nil,
isMyPeer: false,
@ -4555,7 +4578,6 @@ public final class VoiceChatController: ViewController {
} else {
self.panGestureArguments = nil
var dismissing = false
self.isExpanded
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
if self.isScheduling {
self.dismissScheduled()
@ -4563,9 +4585,9 @@ public final class VoiceChatController: ViewController {
if case .fullscreen = self.effectiveDisplayMode {
} else {
self.controller?.dismiss(closing: false, manual: true)
dismissing = true
}
}
dismissing = true
} else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) {
if velocity.y > -1500.0 && !self.isFullscreen {
DispatchQueue.main.async {
@ -5403,493 +5425,3 @@ private final class VoiceChatContextReferenceContentSource: ContextReferenceCont
return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white)
final class VoiceChatMainStageContainerNode: ASDisplayNode {
private let context: AccountContext
private let call: PresentationGroupCall
private var currentPeer: (PeerId, String?)?
private var currentPeerEntry: VoiceChatController.Node.PeerEntry?
private var currentVideoNode: GroupVideoNode?
private var candidateVideoNode: GroupVideoNode?
private let backgroundNode: ASDisplayNode
private let topFadeNode: ASImageNode
private let bottomFadeNode: ASImageNode
private let headerNode: ASDisplayNode
private let backButtonNode: HighlightableButtonNode
private let backButtonArrowNode: ASImageNode
private let pinButtonNode: HighlightTrackingButtonNode
private let pinButtonIconNode: ASImageNode
private let pinButtonTitleNode: ImmediateTextNode
private var audioLevelView: VoiceBlobView?
private let audioLevelDisposable = MetaDisposable()
private var avatarNode: AvatarNode
private let titleNode: ImmediateTextNode
private let microphoneNode: VoiceChatMicrophoneNode
private var validLayout: (CGSize, CGFloat, CGFloat, Bool)?
var tapped: (() -> Void)?
var back: (() -> Void)?
var togglePin: (() -> Void)?
var getAudioLevel: ((PeerId) -> Signal<Float, NoError>)?
private let videoReadyDisposable = MetaDisposable()
init(context: AccountContext, call: PresentationGroupCall) {
self.context = context
self.call = call
self.backgroundNode = ASDisplayNode()
self.backgroundNode.alpha = 0.0
self.backgroundNode.backgroundColor = UIColor(rgb: 0x1c1c1e)
self.topFadeNode = ASImageNode()
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
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 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 = ASImageNode()
self.bottomFadeNode.alpha = 0.0
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
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())
})
self.headerNode = ASDisplayNode()
self.headerNode.alpha = 0.0
self.backButtonArrowNode = ASImageNode()
self.backButtonArrowNode.displayWithoutProcessing = true
self.backButtonArrowNode.displaysAsynchronously = false
self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white)
self.backButtonNode = HighlightableButtonNode()
self.pinButtonIconNode = ASImageNode()
self.pinButtonIconNode.displayWithoutProcessing = true
self.pinButtonIconNode.displaysAsynchronously = false
self.pinButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white)
self.pinButtonTitleNode = ImmediateTextNode()
self.pinButtonTitleNode.isHidden = true
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.isHidden = true
self.titleNode = ImmediateTextNode()
self.titleNode.alpha = 0.0
self.titleNode.isUserInteractionEnabled = false
self.microphoneNode = VoiceChatMicrophoneNode()
self.microphoneNode.alpha = 0.0
super.init()
self.clipsToBounds = true
self.cornerRadius = 11.0
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topFadeNode)
self.addSubnode(self.bottomFadeNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.microphoneNode)
self.addSubnode(self.headerNode)
self.headerNode.addSubnode(self.backButtonNode)
self.headerNode.addSubnode(self.backButtonArrowNode)
self.headerNode.addSubnode(self.pinButtonIconNode)
self.headerNode.addSubnode(self.pinButtonTitleNode)
self.headerNode.addSubnode(self.pinButtonNode)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
self.backButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backButtonNode.alpha = 0.4
strongSelf.backButtonArrowNode.alpha = 0.4
} else {
strongSelf.backButtonNode.alpha = 1.0
strongSelf.backButtonArrowNode.alpha = 1.0
strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
self.pinButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.pinButtonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.pinButtonIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.pinButtonTitleNode.alpha = 0.4
strongSelf.pinButtonIconNode.alpha = 0.4
} else {
strongSelf.pinButtonTitleNode.alpha = 1.0
strongSelf.pinButtonIconNode.alpha = 1.0
strongSelf.pinButtonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.pinButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.pinButtonNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside)
}
deinit {
self.videoReadyDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
}
@objc private func tap() {
self.tapped?()
}
@objc private func backPressed() {
self.back?()
}
@objc private func pinPressed() {
self.togglePin?()
}
var animating = false
fileprivate func animateTransitionIn(from sourceNode: ASDisplayNode, transition: ContainedViewLayoutTransition) {
guard let sourceNode = sourceNode as? VoiceChatTileItemNode, let _ = sourceNode.item, let (_, sideInset, bottomInset, _) = self.validLayout else {
return
}
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.titleNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0)
sourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.animating = true
let targetFrame = self.frame
let startLocalFrame = sourceNode.view.convert(sourceNode.bounds, to: self.supernode?.view)
self.update(size: startLocalFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, force: true, transition: .immediate)
self.frame = startLocalFrame
self.update(size: targetFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, force: true, transition: transition)
transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in
self?.animating = false
})
}
fileprivate func animateTransitionOut(to targetNode: ASDisplayNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
guard let (_, sideInset, bottomInset, _) = self.validLayout else {
return
}
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 0.0)
// alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.titleNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.headerNode, alpha: 0.0)
guard let targetNode = targetNode as? VoiceChatTileItemNode, let _ = targetNode.item else {
completion()
return
}
targetNode.fadeNode.isHidden = true
targetNode.isHidden = false
targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.animating = true
let initialFrame = self.frame
let targetFrame = targetNode.view.convert(targetNode.bounds, to: self.supernode?.view)
self.update(size: targetFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, force: true, transition: transition)
transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in
if let strongSelf = self {
completion()
strongSelf.bottomFadeNode.alpha = 0.0
targetNode.fadeNode.isHidden = false
strongSelf.animating = false
strongSelf.frame = initialFrame
strongSelf.update(size: initialFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, transition: .immediate)
}
})
}
private var silenceTimer: SwiftSignalKit.Timer?
fileprivate func update(peerEntry: VoiceChatController.Node.PeerEntry, pinned: Bool) {
let previousPeerEntry = self.currentPeerEntry
self.currentPeerEntry = peerEntry
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.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)
}
}
self.pinButtonTitleNode.isHidden = !pinned
self.pinButtonIconNode.image = !pinned ? generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) : generateTintedImage(image: UIImage(bundleImageName: "Call/Unpin"), color: .white)
var wavesColor = UIColor(rgb: 0x34c759)
if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id {
self.audioLevelView?.removeFromSuperview()
let blobFrame = self.avatarNode.frame.insetBy(dx: -60.0, dy: -60.0)
self.audioLevelDisposable.set((getAudioLevel(peerEntry.peer.id)
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.audioLevelView == nil, value > 0.0 {
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 1.5,
smallBlobRange: (0, 0),
mediumBlobRange: (0.69, 0.87),
bigBlobRange: (0.71, 1.0)
)
audioLevelView.isHidden = strongSelf.currentPeer?.1 != nil
audioLevelView.setColor(wavesColor)
audioLevelView.alpha = 1.0
strongSelf.audioLevelView = audioLevelView
strongSelf.view.insertSubview(audioLevelView, belowSubview: strongSelf.avatarNode.view)
}
let level = min(1.5, max(0.0, CGFloat(value)))
if let audioLevelView = strongSelf.audioLevelView {
audioLevelView.updateLevel(CGFloat(value))
let avatarScale: CGFloat
if value > 0.02 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.13
audioLevelView.setColor(wavesColor, animated: true)
if let silenceTimer = strongSelf.silenceTimer {
silenceTimer.invalidate()
strongSelf.silenceTimer = nil
}
} else {
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?.silenceTimer = nil
}, queue: Queue.mainQueue())
strongSelf.silenceTimer = silenceTimer
silenceTimer.start()
}
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true)
}
}))
}
var muted = false
var state = peerEntry.state
if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute {
state = .listening
}
switch state {
case .listening:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
muted = true
} else {
muted = peerEntry.muteState != nil
}
case .speaking:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
muted = true
} else {
muted = false
}
case .raisedHand, .invited:
muted = true
}
self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: .white), animated: true)
}
fileprivate func update(peer: (peer: PeerId, endpointId: String?)?, waitForFullSize: Bool, completion: (() -> Void)? = nil) {
let previousPeer = self.currentPeer
if previousPeer?.0 == peer?.0 && previousPeer?.1 == peer?.1 {
completion?()
return
}
self.currentPeer = peer
if let (_, endpointId) = peer {
if endpointId != previousPeer?.1 {
if let endpointId = endpointId {
self.avatarNode.isHidden = true
self.audioLevelView?.isHidden = true
self.call.makeIncomingVideoView(endpointId: endpointId, completion: { [weak self] videoView in
Queue.mainQueue().async {
guard let strongSelf = self, let videoView = videoView else {
return
}
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil)
if let currentVideoNode = strongSelf.currentVideoNode {
strongSelf.currentVideoNode = nil
currentVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentVideoNode] _ in
currentVideoNode?.removeFromSupernode()
})
}
strongSelf.currentVideoNode = videoNode
strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.backgroundNode)
if let (size, sideInset, bottomInset, isLandscape) = strongSelf.validLayout {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate)
}
if waitForFullSize {
strongSelf.videoReadyDisposable.set((videoNode.ready
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
Queue.mainQueue().after(0.01) {
completion?()
}
}))
} else {
strongSelf.videoReadyDisposable.set(nil)
completion?()
}
}
})
} else {
self.avatarNode.isHidden = false
self.audioLevelView?.isHidden = false
if let currentVideoNode = self.currentVideoNode {
currentVideoNode.removeFromSupernode()
self.currentVideoNode = nil
}
}
} else {
self.audioLevelView?.isHidden = self.currentPeer?.1 != nil
completion?()
}
} else {
self.videoReadyDisposable.set(nil)
if let currentVideoNode = self.currentVideoNode {
currentVideoNode.removeFromSupernode()
self.currentVideoNode = nil
}
completion?()
}
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isLandscape: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, sideInset, bottomInset, isLandscape)
if self.animating && !force {
return
}
let initialBottomInset = bottomInset
var bottomInset = bottomInset
if !sideInset.isZero {
bottomInset = 14.0
}
if let currentVideoNode = self.currentVideoNode {
transition.updateFrame(node: currentVideoNode, frame: CGRect(origin: CGPoint(), size: size))
currentVideoNode.updateLayout(size: size, isLandscape: isLandscape, transition: transition)
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
let avatarSize = CGSize(width: 180.0, height: 180.0)
let avatarFrame = CGRect(origin: CGPoint(x: (size.width - avatarSize.width) / 2.0, y: (size.height - avatarSize.height) / 2.0), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let audioLevelView = self.audioLevelView {
transition.updatePosition(layer: audioLevelView.layer, position: avatarFrame.center)
}
let animationSize = CGSize(width: 36.0, height: 36.0)
let titleSize = self.titleNode.updateLayout(size)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset + 12.0 + animationSize.width, y: size.height - bottomInset - titleSize.height - 16.0), size: titleSize))
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.width < size.height {
fadeHeight = 140.0
}
transition.updateFrame(node: self.bottomFadeNode, frame: CGRect(x: 0.0, y: size.height - fadeHeight, width: size.width, height: 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))
if let image = self.backButtonArrowNode.image {
transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: sideInset + 9.0, y: 12.0), size: image.size))
}
transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: sideInset + 28.0, y: 13.0), size: backSize))
let unpinSize = self.pinButtonTitleNode.updateLayout(size)
if let image = self.pinButtonIconNode.image {
let offset: CGFloat = sideInset.isZero ? 0.0 : initialBottomInset + 8.0
transition.updateFrame(node: self.pinButtonIconNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - offset, y: 0.0), size: image.size))
transition.updateFrame(node: self.pinButtonTitleNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - unpinSize.width + 4.0 - offset, y: 14.0), size: unpinSize))
transition.updateFrame(node: self.pinButtonNode, frame: CGRect(x: size.width - image.size.width - unpinSize.width - offset, y: 0.0, width: unpinSize.width + image.size.width, height: 44.0))
}
transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 64.0)))
}
}

View File

@ -0,0 +1,595 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import TelegramVoip
import TelegramAudio
import AccountContext
import Postbox
import TelegramCore
import SyncCore
import AppBundle
import PresentationDataUtils
import AvatarNode
import AudioBlob
private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white)
final class VoiceChatMainStageNode: ASDisplayNode {
private let context: AccountContext
private let call: PresentationGroupCall
private var currentPeer: (PeerId, String?)?
private var currentPeerEntry: VoiceChatPeerEntry?
private var currentVideoNode: GroupVideoNode?
private var candidateVideoNode: GroupVideoNode?
private let backgroundNode: ASDisplayNode
private let topFadeNode: ASImageNode
private let bottomFadeNode: ASImageNode
private let headerNode: ASDisplayNode
private let backButtonNode: HighlightableButtonNode
private let backButtonArrowNode: ASImageNode
private let pinButtonNode: HighlightTrackingButtonNode
private let pinButtonIconNode: ASImageNode
private let pinButtonTitleNode: ImmediateTextNode
private var audioLevelView: VoiceBlobView?
private let audioLevelDisposable = MetaDisposable()
private let speakingPeerDisposable = MetaDisposable()
private let speakingAudioLevelDisposable = MetaDisposable()
private var avatarNode: AvatarNode
private let titleNode: ImmediateTextNode
private let microphoneNode: VoiceChatMicrophoneNode
private let speakingContainerNode: ASDisplayNode
private var speakingEffectView: UIVisualEffectView?
private let speakingAvatarNode: AvatarNode
private let speakingTitleNode: ImmediateTextNode
private var speakingAudioLevelView: VoiceBlobView?
private var validLayout: (CGSize, CGFloat, CGFloat, Bool)?
var tapped: (() -> Void)?
var back: (() -> Void)?
var togglePin: (() -> Void)?
var getAudioLevel: ((PeerId) -> Signal<Float, NoError>)?
private let videoReadyDisposable = MetaDisposable()
init(context: AccountContext, call: PresentationGroupCall) {
self.context = context
self.call = call
self.backgroundNode = ASDisplayNode()
self.backgroundNode.alpha = 0.0
self.backgroundNode.backgroundColor = UIColor(rgb: 0x1c1c1e)
self.topFadeNode = ASImageNode()
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
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 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 = ASImageNode()
self.bottomFadeNode.alpha = 0.0
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
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())
})
self.headerNode = ASDisplayNode()
self.headerNode.alpha = 0.0
self.backButtonArrowNode = ASImageNode()
self.backButtonArrowNode.displayWithoutProcessing = true
self.backButtonArrowNode.displaysAsynchronously = false
self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white)
self.backButtonNode = HighlightableButtonNode()
self.pinButtonIconNode = ASImageNode()
self.pinButtonIconNode.displayWithoutProcessing = true
self.pinButtonIconNode.displaysAsynchronously = false
self.pinButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white)
self.pinButtonTitleNode = ImmediateTextNode()
self.pinButtonTitleNode.isHidden = true
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.isHidden = true
self.titleNode = ImmediateTextNode()
self.titleNode.alpha = 0.0
self.titleNode.isUserInteractionEnabled = false
self.microphoneNode = VoiceChatMicrophoneNode()
self.microphoneNode.alpha = 0.0
self.speakingContainerNode = ASDisplayNode()
self.speakingContainerNode.cornerRadius = 19.0
self.speakingAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
self.speakingTitleNode = ImmediateTextNode()
super.init()
self.clipsToBounds = true
self.cornerRadius = 11.0
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topFadeNode)
self.addSubnode(self.bottomFadeNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.microphoneNode)
self.addSubnode(self.headerNode)
self.headerNode.addSubnode(self.backButtonNode)
self.headerNode.addSubnode(self.backButtonArrowNode)
self.headerNode.addSubnode(self.pinButtonIconNode)
self.headerNode.addSubnode(self.pinButtonTitleNode)
self.headerNode.addSubnode(self.pinButtonNode)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
self.backButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backButtonNode.alpha = 0.4
strongSelf.backButtonArrowNode.alpha = 0.4
} else {
strongSelf.backButtonNode.alpha = 1.0
strongSelf.backButtonArrowNode.alpha = 1.0
strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
self.pinButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.pinButtonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.pinButtonIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.pinButtonTitleNode.alpha = 0.4
strongSelf.pinButtonIconNode.alpha = 0.4
} else {
strongSelf.pinButtonTitleNode.alpha = 1.0
strongSelf.pinButtonIconNode.alpha = 1.0
strongSelf.pinButtonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.pinButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.pinButtonNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside)
}
deinit {
self.videoReadyDisposable.dispose()
self.audioLevelDisposable.dispose()
self.speakingPeerDisposable.dispose()
self.speakingAudioLevelDisposable.dispose()
}
override func didLoad() {
super.didLoad()
let speakingEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.speakingContainerNode.view.addSubview(speakingEffectView)
self.speakingEffectView = speakingEffectView
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
}
@objc private func tap() {
self.tapped?()
}
@objc private func backPressed() {
self.back?()
}
@objc private func pinPressed() {
self.togglePin?()
}
var animating = false
func animateTransitionIn(from sourceNode: ASDisplayNode, transition: ContainedViewLayoutTransition) {
guard let sourceNode = sourceNode as? VoiceChatTileItemNode, let _ = sourceNode.item, let (_, sideInset, bottomInset, _) = self.validLayout else {
return
}
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.titleNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0)
sourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.animating = true
let targetFrame = self.frame
let startLocalFrame = sourceNode.view.convert(sourceNode.bounds, to: self.supernode?.view)
self.update(size: startLocalFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, force: true, transition: .immediate)
self.frame = startLocalFrame
self.update(size: targetFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, force: true, transition: transition)
transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in
self?.animating = false
})
}
func animateTransitionOut(to targetNode: ASDisplayNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
guard let (_, sideInset, bottomInset, _) = self.validLayout else {
return
}
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 0.0)
// alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.titleNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.headerNode, alpha: 0.0)
guard let targetNode = targetNode as? VoiceChatTileItemNode, let _ = targetNode.item else {
completion()
return
}
targetNode.fadeNode.isHidden = true
targetNode.isHidden = false
targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.animating = true
let initialFrame = self.frame
let targetFrame = targetNode.view.convert(targetNode.bounds, to: self.supernode?.view)
self.update(size: targetFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, force: true, transition: transition)
transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in
if let strongSelf = self {
completion()
strongSelf.bottomFadeNode.alpha = 0.0
targetNode.fadeNode.isHidden = false
strongSelf.animating = false
strongSelf.frame = initialFrame
strongSelf.update(size: initialFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: true, transition: .immediate)
}
})
}
private var speakingPeerId: PeerId?
func update(speakingPeerId: PeerId?) {
guard self.speakingPeerId != speakingPeerId else {
return
}
var wavesColor = UIColor(rgb: 0x34c759)
if let getAudioLevel = self.getAudioLevel, let peerId = speakingPeerId {
self.speakingAudioLevelView?.removeFromSuperview()
let blobFrame = self.speakingAvatarNode.frame.insetBy(dx: -14.0, dy: -14.0)
self.speakingAudioLevelDisposable.set((getAudioLevel(peerId)
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.speakingAudioLevelView == nil, value > 0.0 {
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 1.5,
smallBlobRange: (0, 0),
mediumBlobRange: (0.69, 0.87),
bigBlobRange: (0.71, 1.0)
)
audioLevelView.isHidden = strongSelf.currentPeer?.1 != nil
audioLevelView.setColor(wavesColor)
audioLevelView.alpha = 1.0
strongSelf.speakingAudioLevelView = audioLevelView
strongSelf.speakingContainerNode.view.insertSubview(audioLevelView, belowSubview: strongSelf.speakingAvatarNode.view)
}
let level = min(1.5, max(0.0, CGFloat(value)))
if let audioLevelView = strongSelf.speakingAudioLevelView {
audioLevelView.updateLevel(CGFloat(value))
let avatarScale: CGFloat
if value > 0.02 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.13
audioLevelView.setColor(wavesColor, animated: true)
if let silenceTimer = strongSelf.silenceTimer {
silenceTimer.invalidate()
strongSelf.silenceTimer = nil
}
} else {
avatarScale = 1.0
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true)
}
}))
} else {
self.speakingPeerDisposable.set(nil)
if let audioLevelView = self.audioLevelView {
audioLevelView.removeFromSuperview()
self.audioLevelView = nil
}
}
}
private var silenceTimer: SwiftSignalKit.Timer?
func update(peerEntry: VoiceChatPeerEntry, pinned: Bool) {
let previousPeerEntry = self.currentPeerEntry
self.currentPeerEntry = peerEntry
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.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)
}
}
self.pinButtonTitleNode.isHidden = !pinned
self.pinButtonIconNode.image = !pinned ? generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) : generateTintedImage(image: UIImage(bundleImageName: "Call/Unpin"), color: .white)
var wavesColor = UIColor(rgb: 0x34c759)
if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id {
self.audioLevelView?.removeFromSuperview()
let blobFrame = self.avatarNode.frame.insetBy(dx: -60.0, dy: -60.0)
self.audioLevelDisposable.set((getAudioLevel(peerEntry.peer.id)
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.audioLevelView == nil, value > 0.0 {
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 1.5,
smallBlobRange: (0, 0),
mediumBlobRange: (0.69, 0.87),
bigBlobRange: (0.71, 1.0)
)
audioLevelView.isHidden = strongSelf.currentPeer?.1 != nil
audioLevelView.setColor(wavesColor)
audioLevelView.alpha = 1.0
strongSelf.audioLevelView = audioLevelView
strongSelf.view.insertSubview(audioLevelView, belowSubview: strongSelf.avatarNode.view)
}
let level = min(1.5, max(0.0, CGFloat(value)))
if let audioLevelView = strongSelf.audioLevelView {
audioLevelView.updateLevel(CGFloat(value))
let avatarScale: CGFloat
if value > 0.02 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.13
audioLevelView.setColor(wavesColor, animated: true)
if let silenceTimer = strongSelf.silenceTimer {
silenceTimer.invalidate()
strongSelf.silenceTimer = nil
}
} else {
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?.silenceTimer = nil
}, queue: Queue.mainQueue())
strongSelf.silenceTimer = silenceTimer
silenceTimer.start()
}
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true)
}
}))
}
var muted = false
var state = peerEntry.state
if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute {
state = .listening
}
switch state {
case .listening:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
muted = true
} else {
muted = peerEntry.muteState != nil
}
case .speaking:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
muted = true
} else {
muted = false
}
case .raisedHand, .invited:
muted = true
}
self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: .white), animated: true)
}
func update(peer: (peer: PeerId, endpointId: String?)?, waitForFullSize: Bool, completion: (() -> Void)? = nil) {
let previousPeer = self.currentPeer
if previousPeer?.0 == peer?.0 && previousPeer?.1 == peer?.1 {
completion?()
return
}
self.currentPeer = peer
if let (_, endpointId) = peer {
if endpointId != previousPeer?.1 {
if let endpointId = endpointId {
self.avatarNode.isHidden = true
self.audioLevelView?.isHidden = true
self.call.makeIncomingVideoView(endpointId: endpointId, completion: { [weak self] videoView in
Queue.mainQueue().async {
guard let strongSelf = self, let videoView = videoView else {
return
}
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil)
if let currentVideoNode = strongSelf.currentVideoNode {
strongSelf.currentVideoNode = nil
currentVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentVideoNode] _ in
currentVideoNode?.removeFromSupernode()
})
}
strongSelf.currentVideoNode = videoNode
strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.backgroundNode)
if let (size, sideInset, bottomInset, isLandscape) = strongSelf.validLayout {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate)
}
if waitForFullSize {
strongSelf.videoReadyDisposable.set((videoNode.ready
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
Queue.mainQueue().after(0.01) {
completion?()
}
}))
} else {
strongSelf.videoReadyDisposable.set(nil)
completion?()
}
}
})
} else {
self.avatarNode.isHidden = false
self.audioLevelView?.isHidden = false
if let currentVideoNode = self.currentVideoNode {
currentVideoNode.removeFromSupernode()
self.currentVideoNode = nil
}
}
} else {
self.audioLevelView?.isHidden = self.currentPeer?.1 != nil
completion?()
}
} else {
self.videoReadyDisposable.set(nil)
if let currentVideoNode = self.currentVideoNode {
currentVideoNode.removeFromSupernode()
self.currentVideoNode = nil
}
completion?()
}
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isLandscape: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, sideInset, bottomInset, isLandscape)
if self.animating && !force {
return
}
let initialBottomInset = bottomInset
var bottomInset = bottomInset
if !sideInset.isZero {
bottomInset = 14.0
}
if let currentVideoNode = self.currentVideoNode {
transition.updateFrame(node: currentVideoNode, frame: CGRect(origin: CGPoint(), size: size))
currentVideoNode.updateLayout(size: size, isLandscape: isLandscape, transition: transition)
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
let avatarSize = CGSize(width: 180.0, height: 180.0)
let avatarFrame = CGRect(origin: CGPoint(x: (size.width - avatarSize.width) / 2.0, y: (size.height - avatarSize.height) / 2.0), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let audioLevelView = self.audioLevelView {
transition.updatePosition(layer: audioLevelView.layer, position: avatarFrame.center)
}
let animationSize = CGSize(width: 36.0, height: 36.0)
let titleSize = self.titleNode.updateLayout(size)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset + 12.0 + animationSize.width, y: size.height - bottomInset - titleSize.height - 16.0), size: titleSize))
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.width < size.height {
fadeHeight = 140.0
}
transition.updateFrame(node: self.bottomFadeNode, frame: CGRect(x: 0.0, y: size.height - fadeHeight, width: size.width, height: 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))
if let image = self.backButtonArrowNode.image {
transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: sideInset + 9.0, y: 12.0), size: image.size))
}
transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: sideInset + 28.0, y: 13.0), size: backSize))
let unpinSize = self.pinButtonTitleNode.updateLayout(size)
if let image = self.pinButtonIconNode.image {
let offset: CGFloat = sideInset.isZero ? 0.0 : initialBottomInset + 8.0
transition.updateFrame(node: self.pinButtonIconNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - offset, y: 0.0), size: image.size))
transition.updateFrame(node: self.pinButtonTitleNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - unpinSize.width + 4.0 - offset, y: 14.0), size: unpinSize))
transition.updateFrame(node: self.pinButtonNode, frame: CGRect(x: size.width - image.size.width - unpinSize.width - offset, y: 0.0, width: unpinSize.width + image.size.width, height: 44.0))
}
transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 64.0)))
}
}

View File

@ -154,12 +154,12 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode {
self.cornersNode = ASImageNode()
self.cornersNode.displaysAsynchronously = false
self.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: false)
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.addSubnode(self.backgroundNode)
self.addSubnode(self.cornersNode)
}
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
@ -174,6 +174,10 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode {
var backgroundFrame = self.backgroundNode.frame
backgroundFrame.size.height = currentValue
self.backgroundNode.frame = backgroundFrame
var cornersFrame = self.cornersNode.frame
cornersFrame.origin.y = currentValue
self.cornersNode.frame = cornersFrame
}
func asyncLayout() -> (_ item: VoiceChatTilesGridItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) {
@ -191,6 +195,7 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode {
tileGridNode = current
} else {
strongSelf.backgroundNode.backgroundColor = item.getIsExpanded() ? fullscreenBackgroundColor : panelBackgroundColor
strongSelf.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: item.getIsExpanded())
tileGridNode = VoiceChatTileGridNode(context: item.context)
strongSelf.addSubnode(tileGridNode)
@ -202,6 +207,7 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode {
if currentItem == nil {
tileGridNode.frame = CGRect(x: params.leftInset, y: 0.0, width: tileGridSize.width, height: 0.0)
strongSelf.backgroundNode.frame = tileGridNode.frame
strongSelf.cornersNode.frame = CGRect(x: 14.0, y: 0.0, width: tileGridSize.width, height: 50.0)
} else {
transition.updateFrame(node: tileGridNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize))
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize))