import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import SyncCore import Postbox import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle import SwiftSignalKit import AnimatedAvatarSetNode private let titleFont = Font.semibold(15.0) private let subtitleFont = Font.regular(13.0) public enum GroupCallPanelSource { case none case all case peer(PeerId) } public final class GroupCallPanelData { public let peerId: PeerId public let info: GroupCallInfo public let topParticipants: [GroupCallParticipantsContext.Participant] public let participantCount: Int public let numberOfActiveSpeakers: Int public let groupCall: PresentationGroupCall? public init( peerId: PeerId, info: GroupCallInfo, topParticipants: [GroupCallParticipantsContext.Participant], participantCount: Int, numberOfActiveSpeakers: Int, groupCall: PresentationGroupCall? ) { self.peerId = peerId self.info = info self.topParticipants = topParticipants self.participantCount = participantCount self.numberOfActiveSpeakers = numberOfActiveSpeakers self.groupCall = groupCall } } public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { private let context: AccountContext private var theme: PresentationTheme private var strings: PresentationStrings private let tapAction: () -> Void private let contentNode: ASDisplayNode private let tapButton: HighlightTrackingButtonNode private let joinButton: HighlightableButtonNode private let joinButtonTitleNode: ImmediateTextNode private let joinButtonBackgroundNode: ASImageNode private let micButton: HighlightTrackingButtonNode private let micButtonForegroundNode: VoiceChatMicrophoneNode private let micButtonBackgroundNode: ASImageNode let titleNode: ImmediateTextNode let textNode: ImmediateTextNode private let muteIconNode: ASImageNode private let avatarsContext: AnimatedAvatarSetContext private var avatarsContent: AnimatedAvatarSetContext.Content? private let avatarsNode: AnimatedAvatarSetNode private let separatorNode: ASDisplayNode private let membersDisposable = MetaDisposable() private let isMutedDisposable = MetaDisposable() private var currentData: GroupCallPanelData? private var validLayout: (CGSize, CGFloat, CGFloat)? public init(context: AccountContext, presentationData: PresentationData, tapAction: @escaping () -> Void) { self.context = context self.theme = presentationData.theme self.strings = presentationData.strings self.tapAction = tapAction self.contentNode = ASDisplayNode() self.tapButton = HighlightTrackingButtonNode() self.joinButton = HighlightableButtonNode() self.joinButtonTitleNode = ImmediateTextNode() self.joinButtonBackgroundNode = ASImageNode() self.micButton = HighlightTrackingButtonNode() self.micButtonForegroundNode = VoiceChatMicrophoneNode() self.micButtonBackgroundNode = ASImageNode() self.titleNode = ImmediateTextNode() self.textNode = ImmediateTextNode() self.muteIconNode = ASImageNode() self.avatarsContext = AnimatedAvatarSetContext() self.avatarsNode = AnimatedAvatarSetNode() self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true super.init() self.addSubnode(self.contentNode) self.tapButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 strongSelf.textNode.layer.removeAnimation(forKey: "opacity") strongSelf.textNode.alpha = 0.4 } else { strongSelf.titleNode.alpha = 1.0 strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.textNode.alpha = 1.0 strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.contentNode.addSubnode(self.titleNode) self.contentNode.addSubnode(self.textNode) self.contentNode.addSubnode(self.muteIconNode) self.contentNode.addSubnode(self.avatarsNode) self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.contentNode.addSubnode(self.tapButton) self.joinButton.addSubnode(self.joinButtonBackgroundNode) self.joinButton.addSubnode(self.joinButtonTitleNode) self.contentNode.addSubnode(self.joinButton) self.joinButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.micButton.addSubnode(self.micButtonBackgroundNode) self.micButton.addSubnode(self.micButtonForegroundNode) self.contentNode.addSubnode(self.micButton) self.micButton.addTarget(self, action: #selector(self.micTapped), forControlEvents: [.touchUpInside]) self.contentNode.addSubnode(self.separatorNode) self.updatePresentationData(presentationData) } deinit { self.membersDisposable.dispose() self.isMutedDisposable.dispose() } public override func didLoad() { super.didLoad() let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.micButtonPressGesture(_:))) longTapRecognizer.minimumPressDuration = 0.01 self.micButton.view.addGestureRecognizer(longTapRecognizer) } @objc private func tapped() { self.tapAction() } @objc private func micTapped() { guard let call = self.currentData?.groupCall else { return } call.toggleIsMuted() } private var actionButtonPressGestureStartTime: Double = 0.0 @objc private func micButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) { guard let call = self.currentData?.groupCall else { return } switch gestureRecognizer.state { case .began: self.actionButtonPressGestureStartTime = CACurrentMediaTime() call.setIsMuted(action: .muted(isPushToTalkActive: true)) case .ended, .cancelled: let timestamp = CACurrentMediaTime() if timestamp - self.actionButtonPressGestureStartTime < 0.2 { call.toggleIsMuted() } else { call.setIsMuted(action: .muted(isPushToTalkActive: false)) } default: break } } func updatePresentationData(_ presentationData: PresentationData) { self.theme = presentationData.theme self.strings = presentationData.strings self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor self.theme = presentationData.theme self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor self.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor) self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor) //TODO:localize self.micButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: UIColor(rgb: 0x30b251)) self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_Title, font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.primaryTextColor) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor) self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme) } public func update(data: GroupCallPanelData) { let previousData = self.currentData self.currentData = data if previousData?.groupCall !== data.groupCall { let membersText: String if data.participantCount == 0 { membersText = self.strings.PeopleNearby_NoMembers } else { membersText = self.strings.Conversation_StatusMembers(Int32(data.participantCount)) } self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) self.membersDisposable.set(nil) self.isMutedDisposable.set(nil) if let groupCall = data.groupCall { self.membersDisposable.set((groupCall.summaryState |> deliverOnMainQueue).start(next: { [weak self] summaryState in guard let strongSelf = self, let summaryState = summaryState else { return } let membersText: String let membersTextIsActive: Bool if summaryState.numberOfActiveSpeakers != 0 { membersText = strongSelf.strings.VoiceChat_StatusMembersSpeaking(Int32(summaryState.numberOfActiveSpeakers)) membersTextIsActive = true } else { if summaryState.participantCount == 0 { membersText = strongSelf.strings.PeopleNearby_NoMembers } else { membersText = strongSelf.strings.Conversation_StatusMembers(Int32(summaryState.participantCount)) } membersTextIsActive = false } strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? strongSelf.theme.chat.inputPanel.panelControlAccentColor : strongSelf.theme.chat.inputPanel.secondaryTextColor) strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false) if let (size, leftInset, rightInset) = strongSelf.validLayout { strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) } })) self.isMutedDisposable.set((groupCall.isMuted |> deliverOnMainQueue).start(next: { [weak self] isMuted in guard let strongSelf = self else { return } strongSelf.micButtonForegroundNode.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, color: UIColor.white), animated: true) })) } } else if data.groupCall == nil { let membersText: String let membersTextIsActive: Bool if data.numberOfActiveSpeakers != 0 { membersText = self.strings.VoiceChat_StatusMembersSpeaking(Int32(data.numberOfActiveSpeakers)) membersTextIsActive = true } else { if data.participantCount == 0 { membersText = self.strings.PeopleNearby_NoMembers } else { membersText = self.strings.Conversation_StatusMembers(Int32(data.participantCount)) } membersTextIsActive = false } self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? self.theme.chat.inputPanel.panelControlAccentColor : self.theme.chat.inputPanel.secondaryTextColor) self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) } if let (size, leftInset, rightInset) = self.validLayout { self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) } } public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, leftInset, rightInset) let panelHeight = size.height transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: size)) self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width - 7.0 - 36.0 - 7.0, height: panelHeight)) if let avatarsContent = self.avatarsContent { let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true) transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)) } let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude)) let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0) let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize) transition.updateFrame(node: self.joinButton, frame: joinButtonFrame) transition.updateFrame(node: self.joinButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: joinButtonFrame.size)) transition.updateFrame(node: self.joinButtonTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((joinButtonFrame.width - joinButtonTitleSize.width) / 2.0), y: floorToScreenPixels((joinButtonFrame.height - joinButtonTitleSize.height) / 2.0)), size: joinButtonTitleSize)) let micButtonSize = CGSize(width: 36.0, height: 36.0) let micButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - micButtonSize.width, y: floor((panelHeight - micButtonSize.height) / 2.0)), size: micButtonSize) transition.updateFrame(node: self.micButton, frame: micButtonFrame) transition.updateFrame(node: self.micButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: micButtonFrame.size)) let animationSize = CGSize(width: 36.0, height: 36.0) transition.updateFrame(node: self.micButtonForegroundNode, frame: CGRect(origin: CGPoint(x: floor((micButtonFrame.width - animationSize.width) / 2.0), y: floor((micButtonFrame.height - animationSize.height) / 2.0)), size: animationSize)) let titleSize = self.titleNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: 9.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: textSize)) if let image = self.muteIconNode.image { transition.updateFrame(node: self.muteIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + 5.0), size: image.size)) } self.muteIconNode.isHidden = self.currentData?.groupCall != nil self.joinButton.isHidden = self.currentData?.groupCall != nil self.micButton.isHidden = self.currentData?.groupCall == nil transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) } public func animateIn(_ transition: ContainedViewLayoutTransition) { self.clipsToBounds = true let contentPosition = self.contentNode.layer.position transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 50.0), completion: { [weak self] _ in self?.clipsToBounds = false }) } public func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { self.clipsToBounds = true let contentPosition = self.contentNode.layer.position transition.animatePosition(node: self.contentNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 50.0), removeOnCompletion: false, completion: { [weak self] _ in self?.clipsToBounds = false completion() }) } func rightButtonSnapshotViews() -> (background: UIView, foreground: UIView)? { if !self.joinButton.isHidden { if let foregroundView = self.joinButtonTitleNode.view.snapshotContentTree() { let backgroundFrame = self.joinButtonBackgroundNode.view.convert(self.joinButtonBackgroundNode.bounds, to: nil) let foregroundFrame = self.joinButtonTitleNode.view.convert(self.joinButtonTitleNode.bounds, to: nil) let backgroundView = UIView() backgroundView.backgroundColor = self.theme.chat.inputPanel.actionControlFillColor backgroundView.frame = backgroundFrame backgroundView.layer.cornerRadius = backgroundFrame.height / 2.0 foregroundView.frame = foregroundFrame return (backgroundView, foregroundView) } } else if !self.micButton.isHidden { if let foregroundView = self.micButtonForegroundNode.view.snapshotContentTree() { let backgroundFrame = self.micButtonBackgroundNode.view.convert(self.micButtonBackgroundNode.bounds, to: nil) let foregroundFrame = self.micButtonForegroundNode.view.convert(self.micButtonForegroundNode.bounds, to: nil) let backgroundView = UIView() backgroundView.backgroundColor = UIColor(rgb: 0x30b251) backgroundView.frame = backgroundFrame backgroundView.layer.cornerRadius = backgroundFrame.height / 2.0 foregroundView.frame = foregroundFrame return (backgroundView, foregroundView) } } return nil } }