diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index d1a31df45f..e4c9e7ebd8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -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) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index 7e6530d813..dcfc9d67fd 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -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) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 788cb9e8e2..5e6e2dba11 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -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(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)? - - 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))) - } -} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift new file mode 100644 index 0000000000..b20e47b41f --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -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)? + + 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))) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift index 5ba41c62a4..add9c2444d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift @@ -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))