diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 78df70cfaf..bdaf204d3b 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AsyncDisplayKit import Postbox import TelegramCore import SyncCore @@ -568,7 +569,7 @@ public protocol SharedAccountContext: class { func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController - func navigateToCurrentCall() + func navigateToCurrentCall(sourcePanel: ASDisplayNode?) var hasOngoingCall: ValuePromise { get } var immediateHasOngoingCall: Bool { get } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 46931b960d..b861fe2348 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AsyncDisplayKit import Postbox import TelegramCore import SyncCore @@ -246,6 +247,8 @@ public protocol PresentationGroupCall: class { func invitePeer(_ peerId: PeerId) var invitedPeers: Signal, NoError> { get } + + var sourcePanel: ASDisplayNode? { get set } } public protocol PresentationCallManager: class { @@ -253,5 +256,5 @@ public protocol PresentationCallManager: class { var currentGroupCallSignal: Signal { get } func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult - func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, endCurrentIfAny: Bool) -> RequestOrJoinGroupCallResult + func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, endCurrentIfAny: Bool, sourcePanel: ASDisplayNode?) -> RequestOrJoinGroupCallResult } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index ee2abb052d..c5d3e28462 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -281,7 +281,7 @@ public final class CallListController: ViewController { if case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peerId { began?() - strongSelf.context.sharedContext.navigateToCurrentCall() + strongSelf.context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = strongSelf.presentationData let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index ac5fc8a19d..ba662e74ff 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -126,7 +126,7 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peerId { - context.sharedContext.navigateToCurrentCall() + context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index cae45845de..9903d9cfed 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -391,12 +391,17 @@ open class NavigationController: UINavigationController, ContainableController, } if let inCallStatusBar = self.inCallStatusBar { - let inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(layout.statusBarHeight ?? 0.0, max(40.0, layout.safeInsets.top)))) + var inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(layout.statusBarHeight ?? 0.0, max(40.0, layout.safeInsets.top)))) + if layout.deviceMetrics.hasTopNotch { + inCallStatusBarFrame.size.height += 12.0 + } if inCallStatusBar.frame.isEmpty { inCallStatusBar.frame = inCallStatusBarFrame } else { transition.updateFrame(node: inCallStatusBar, frame: inCallStatusBarFrame) } + inCallStatusBar.callStatusBarNode?.update(size: inCallStatusBarFrame.size) + inCallStatusBar.callStatusBarNode?.frame = inCallStatusBarFrame layout.statusBarHeight = inCallStatusBarFrame.height self.inCallStatusBar?.frame = inCallStatusBarFrame } @@ -1339,13 +1344,14 @@ open class NavigationController: UINavigationController, ContainableController, } } - public func setForceInCallStatusBar(_ forceInCallStatusBarText: String?, transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)) { - if let forceInCallStatusBarText = forceInCallStatusBarText { + public func setForceInCallStatusBar(_ forceInCallStatusBar: CallStatusBarNode?, transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)) { + if let forceInCallStatusBar = forceInCallStatusBar { let inCallStatusBar: StatusBar if let current = self.inCallStatusBar { inCallStatusBar = current } else { inCallStatusBar = StatusBar() + inCallStatusBar.clipsToBounds = false inCallStatusBar.inCallNavigate = { [weak self] in self?.scrollToTop(.master) } @@ -1371,7 +1377,7 @@ open class NavigationController: UINavigationController, ContainableController, } if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: transition) - inCallStatusBar.updateState(statusBar: nil, withSafeInsets: !layout.safeInsets.top.isZero, inCallText: forceInCallStatusBarText, animated: false) + inCallStatusBar.updateState(statusBar: nil, withSafeInsets: !layout.safeInsets.top.isZero, inCallNode: forceInCallStatusBar, animated: false) } } else if let inCallStatusBar = self.inCallStatusBar { self.inCallStatusBar = nil diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 8cee266195..9066fe5d09 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -269,7 +269,7 @@ open class NavigationBar: ASDisplayNode { private var title: String? { didSet { if let title = self.title { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.presentationData.theme.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) self.titleNode.accessibilityLabel = title if self.titleNode.supernode == nil { self.buttonsContainerNode.addSubnode(self.titleNode) diff --git a/submodules/Display/Source/StatusBar.swift b/submodules/Display/Source/StatusBar.swift index c7cc1fe8ba..84b64f8711 100644 --- a/submodules/Display/Source/StatusBar.swift +++ b/submodules/Display/Source/StatusBar.swift @@ -25,7 +25,11 @@ public class StatusBarSurface { } } -private let inCallBackgroundColor = UIColor(rgb: 0x43d551) +open class CallStatusBarNode: ASDisplayNode { + open func update(size: CGSize) { + + } +} private func addInCallAnimation(_ layer: CALayer) { let animation = CAKeyframeAnimation(keyPath: "opacity") @@ -92,10 +96,7 @@ public final class StatusBar: ASDisplayNode { private var removeProxyNodeScheduled = false let offsetNode = ASDisplayNode() - private let inCallBackgroundNode = ASDisplayNode() - private let inCallLabel: StatusBarLabelNode - - private var inCallText: String? = nil + var callStatusBarNode: CallStatusBarNode? = nil public var verticalOffset: CGFloat = 0.0 { didSet { @@ -113,14 +114,8 @@ public final class StatusBar: ASDisplayNode { } public override init() { - self.inCallLabel = StatusBarLabelNode() - self.inCallLabel.isUserInteractionEnabled = false - self.offsetNode.isUserInteractionEnabled = false - let labelSize = self.inCallLabel.updateLayout(CGSize(width: 300.0, height: 300.0)) - self.inCallLabel.frame = CGRect(origin: CGPoint(x: 10.0, y: 20.0 + 4.0), size: labelSize) - super.init() self.setViewBlock({ @@ -130,18 +125,17 @@ public final class StatusBar: ASDisplayNode { (self.view as! StatusBarView).node = self self.addSubnode(self.offsetNode) - self.addSubnode(self.inCallBackgroundNode) self.clipsToBounds = true self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func updateState(statusBar: UIView?, withSafeInsets: Bool, inCallText: String?, animated: Bool) { + func updateState(statusBar: UIView?, withSafeInsets: Bool, inCallNode: CallStatusBarNode?, animated: Bool) { if let statusBar = statusBar { self.removeProxyNodeScheduled = false let resolvedStyle: StatusBarStyle - if inCallText != nil && !self.ignoreInCall { + if inCallNode != nil && !self.ignoreInCall { resolvedStyle = .White } else { resolvedStyle = self.statusBarStyle @@ -176,46 +170,35 @@ public final class StatusBar: ASDisplayNode { ignoreInCall = true } - var resolvedInCallText: String? = inCallText + var resolvedCallStatusBarNode: CallStatusBarNode? = inCallNode if ignoreInCall { - resolvedInCallText = nil + resolvedCallStatusBarNode = nil } - if (resolvedInCallText != nil) != (self.inCallText != nil) { - if let _ = resolvedInCallText { - if !withSafeInsets { - self.addSubnode(self.inCallLabel) - } - addInCallAnimation(self.inCallLabel.layer) - - self.inCallBackgroundNode.layer.backgroundColor = inCallBackgroundColor.cgColor + if (resolvedCallStatusBarNode != nil) != (self.callStatusBarNode != nil) { + if let resolvedCallStatusBarNode = resolvedCallStatusBarNode { + self.addSubnode(resolvedCallStatusBarNode) if animated { - self.inCallBackgroundNode.layer.animate(from: UIColor.clear.cgColor, to: inCallBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3) + resolvedCallStatusBarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - } else { - self.inCallLabel.removeFromSupernode() + } else if let callStatusBarNode = self.callStatusBarNode { + self.callStatusBarNode = nil - self.inCallBackgroundNode.layer.backgroundColor = UIColor.clear.cgColor if animated { - self.inCallBackgroundNode.layer.animate(from: inCallBackgroundColor.cgColor, to: UIColor.clear.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3) + callStatusBarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak callStatusBarNode] _ in + callStatusBarNode?.removeFromSupernode() + }) + } else { + callStatusBarNode.removeFromSupernode() } } } - - if let resolvedInCallText = resolvedInCallText { - if self.inCallText != resolvedInCallText { - self.inCallLabel.attributedText = NSAttributedString(string: resolvedInCallText, font: Font.regular(14.0), textColor: .white) - } - - self.layoutInCallLabel() - } - - self.inCallText = resolvedInCallText + self.callStatusBarNode = resolvedCallStatusBarNode } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) && self.inCallText != nil { + if self.bounds.contains(point) && self.callStatusBarNode != nil { return self.view } else { return nil @@ -223,42 +206,8 @@ public final class StatusBar: ASDisplayNode { } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state, self.inCallText != nil { + if case .ended = recognizer.state, self.callStatusBarNode != nil { self.inCallNavigate?() } } - - override public func layout() { - super.layout() - - self.layoutInCallLabel() - } - - override public var frame: CGRect { - didSet { - if oldValue.size != self.frame.size { - let bounds = self.bounds - self.inCallBackgroundNode.frame = CGRect(origin: CGPoint(), size: bounds.size) - } - } - } - - override public var bounds: CGRect { - didSet { - if oldValue.size != self.bounds.size { - let bounds = self.bounds - self.inCallBackgroundNode.frame = CGRect(origin: CGPoint(), size: bounds.size) - } - } - } - - private func layoutInCallLabel() { - if self.inCallLabel.supernode != nil { - let size = self.bounds.size - if !size.width.isZero && !size.height.isZero { - let labelSize = self.inCallLabel.updateLayout(size) - self.inCallLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 20.0 + floor((20.0 - labelSize.height) / 2.0)), size: labelSize) - } - } - } } diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index fa0f13c85b..9515c46a69 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -880,7 +880,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: user.id, isVideo: false, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == user.id { - context.sharedContext.navigateToCurrentCall() + context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index 98fc345ac9..af3a6e2845 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoController.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoController.swift @@ -876,7 +876,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peer.id { - context.sharedContext.navigateToCurrentCall() + context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in diff --git a/submodules/TelegramBaseController/BUILD b/submodules/TelegramBaseController/BUILD index 4d8caa9769..dfa86f0840 100644 --- a/submodules/TelegramBaseController/BUILD +++ b/submodules/TelegramBaseController/BUILD @@ -21,7 +21,6 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/Markdown:Markdown", "//submodules/TelegramCallsUI:TelegramCallsUI", - "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index e2924dc4e3..a7ff5e7c1f 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -12,6 +12,7 @@ import UniversalMediaPlayer import AccountContext import OverlayStatusController import PresentationDataUtils +import TelegramCallsUI public enum MediaAccessoryPanelVisibility { case none @@ -25,34 +26,6 @@ public enum LocationBroadcastPanelSource { case peer(PeerId) } -public enum GroupCallPanelSource { - case none - case all - case peer(PeerId) -} - -final class GroupCallPanelData { - let peerId: PeerId - let info: GroupCallInfo - let topParticipants: [GroupCallParticipantsContext.Participant] - let participantCount: Int - let groupCall: PresentationGroupCall? - - init( - peerId: PeerId, - info: GroupCallInfo, - topParticipants: [GroupCallParticipantsContext.Participant], - participantCount: Int, - groupCall: PresentationGroupCall? - ) { - self.peerId = peerId - self.info = info - self.topParticipants = topParticipants - self.participantCount = participantCount - self.groupCall = groupCall - } -} - private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) { let presentImpl: (Message?) -> Void = { [weak controller] message in if let message = message, let strongController = controller { @@ -430,7 +403,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } strongSelf.joinGroupCall( peerId: groupCallPanelData.peerId, - info: groupCallPanelData.info + info: groupCallPanelData.info, + sourcePanel: strongSelf.groupCallAccessoryPanel ) }) if let navigationBar = self.navigationBar { @@ -847,11 +821,11 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { })] } - private func joinGroupCall(peerId: PeerId, info: GroupCallInfo) { - let callResult = self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash), endCurrentIfAny: false) + private func joinGroupCall(peerId: PeerId, info: GroupCallInfo, sourcePanel: GroupCallNavigationAccessoryPanel?) { + let callResult = self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash), endCurrentIfAny: false, sourcePanel: sourcePanel) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peerId { - self.context.sharedContext.navigateToCurrentCall() + self.context.sharedContext.navigateToCurrentCall(sourcePanel: sourcePanel) } else { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in @@ -867,7 +841,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { if let current = current { strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { if let strongSelf = self { - let _ = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash), endCurrentIfAny: true) + let _ = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash), endCurrentIfAny: true, sourcePanel: sourcePanel) } })]), in: .window(.root)) } else { diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index 12fb51659d..cc2b6fa94f 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/AnimationUI:AnimationUI", "//submodules/UndoUI:UndoUI", "//submodules/AudioBlob:AudioBlob", + "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index 93b96e2d90..6c5651cc17 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -1238,7 +1238,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro if !self.shouldStayHiddenUntilConnection || self.containerNode.alpha > 0.0 { self.containerNode.layer.allowsGroupOpacity = true self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in - self?.containerNode.layer.allowsGroupOpacity = true + self?.containerNode.layer.allowsGroupOpacity = false }) self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in completion() diff --git a/submodules/TelegramBaseController/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift similarity index 78% rename from submodules/TelegramBaseController/Sources/GroupCallNavigationAccessoryPanel.swift rename to submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift index b1fedb32eb..0cdfc1326f 100644 --- a/submodules/TelegramBaseController/Sources/GroupCallNavigationAccessoryPanel.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift @@ -15,7 +15,35 @@ import AnimatedAvatarSetNode private let titleFont = Font.semibold(15.0) private let subtitleFont = Font.regular(13.0) -final class GroupCallNavigationAccessoryPanel: ASDisplayNode { +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 groupCall: PresentationGroupCall? + + public init( + peerId: PeerId, + info: GroupCallInfo, + topParticipants: [GroupCallParticipantsContext.Participant], + participantCount: Int, + groupCall: PresentationGroupCall? + ) { + self.peerId = peerId + self.info = info + self.topParticipants = topParticipants + self.participantCount = participantCount + self.groupCall = groupCall + } +} + +public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { private let context: AccountContext private var theme: PresentationTheme private var strings: PresentationStrings @@ -31,12 +59,11 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { private let joinButtonBackgroundNode: ASImageNode private let micButton: HighlightTrackingButtonNode - private let micButtonForegroundMutedNode: ASImageNode - private let micButtonForegroundUnmutedNode: ASImageNode + private let micButtonForegroundNode: VoiceChatMicrophoneNode private let micButtonBackgroundNode: ASImageNode - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode + let titleNode: ImmediateTextNode + let textNode: ImmediateTextNode private let muteIconNode: ASImageNode private let avatarsContext: AnimatedAvatarSetContext @@ -51,7 +78,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { private var currentData: GroupCallPanelData? private var validLayout: (CGSize, CGFloat, CGFloat)? - init(context: AccountContext, presentationData: PresentationData, tapAction: @escaping () -> Void) { + public init(context: AccountContext, presentationData: PresentationData, tapAction: @escaping () -> Void) { self.context = context self.theme = presentationData.theme self.strings = presentationData.strings @@ -67,8 +94,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.joinButtonBackgroundNode = ASImageNode() self.micButton = HighlightTrackingButtonNode() - self.micButtonForegroundMutedNode = ASImageNode() - self.micButtonForegroundUnmutedNode = ASImageNode() + self.micButtonForegroundNode = VoiceChatMicrophoneNode() self.micButtonBackgroundNode = ASImageNode() self.titleNode = ImmediateTextNode() @@ -117,8 +143,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.joinButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.micButton.addSubnode(self.micButtonBackgroundNode) - self.micButton.addSubnode(self.micButtonForegroundMutedNode) - self.micButton.addSubnode(self.micButtonForegroundUnmutedNode) + self.micButton.addSubnode(self.micButtonForegroundNode) self.contentNode.addSubnode(self.micButton) self.micButton.addTarget(self, action: #selector(self.micTapped), forControlEvents: [.touchUpInside]) @@ -132,7 +157,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.isMutedDisposable.dispose() } - override func didLoad() { + public override func didLoad() { super.didLoad() let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.micButtonPressGesture(_:))) @@ -187,9 +212,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { 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.micButtonForegroundMutedNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: .white) - self.micButtonForegroundUnmutedNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: .white) + self.micButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: UIColor(rgb: 0x30b251)) //TODO:localize self.titleNode.attributedText = NSAttributedString(string: "Voice Chat", font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.primaryTextColor) @@ -198,7 +221,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme) } - func update(data: GroupCallPanelData) { + public func update(data: GroupCallPanelData) { let previousData = self.currentData self.currentData = data @@ -244,8 +267,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { guard let strongSelf = self else { return } - strongSelf.micButtonForegroundMutedNode.isHidden = !isMuted - strongSelf.micButtonForegroundUnmutedNode.isHidden = isMuted + strongSelf.micButtonForegroundNode.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, color: UIColor.white), animated: true) })) } } else if data.groupCall == nil { @@ -266,7 +288,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { } } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, leftInset, rightInset) let panelHeight = size.height @@ -291,17 +313,14 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { 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)) - if let image = self.micButtonForegroundMutedNode.image { - transition.updateFrame(node: self.micButtonForegroundMutedNode, frame: CGRect(origin: CGPoint(x: floor((micButtonFrame.width - image.size.width) / 2.0), y: floor((micButtonFrame.height - image.size.height) / 2.0)), size: image.size)) - } - if let image = self.micButtonForegroundUnmutedNode.image { - transition.updateFrame(node: self.micButtonForegroundUnmutedNode, frame: CGRect(origin: CGPoint(x: floor((micButtonFrame.width - image.size.width) / 2.0), y: floor((micButtonFrame.height - image.size.height) / 2.0)), size: image.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: 10.0), size: titleSize) + 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)) @@ -315,7 +334,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) } - func animateIn(_ transition: ContainedViewLayoutTransition) { + 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 @@ -323,7 +342,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { }) } - func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + 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 @@ -331,4 +350,36 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode { 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 + } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index e828278d00..77657f9d6a 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -1,4 +1,5 @@ import Foundation +import AsyncDisplayKit import Postbox import TelegramCore import SyncCore @@ -592,9 +593,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } } - public func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, endCurrentIfAny: Bool) -> RequestOrJoinGroupCallResult { + public func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, endCurrentIfAny: Bool, sourcePanel: ASDisplayNode?) -> RequestOrJoinGroupCallResult { let begin: () -> Void = { [weak self] in - let _ = self?.startGroupCall(accountContext: context, peerId: peerId, initialCall: initialCall).start() + let _ = self?.startGroupCall(accountContext: context, peerId: peerId, initialCall: initialCall, sourcePanel: sourcePanel).start() } if let currentGroupCall = self.currentGroupCallValue { if endCurrentIfAny { @@ -618,7 +619,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager { accountContext: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, - internalId: CallSessionInternalId = CallSessionInternalId() + internalId: CallSessionInternalId = CallSessionInternalId(), + sourcePanel: ASDisplayNode? ) -> Signal { let (presentationData, present, openSettings) = self.getDeviceAccessData() @@ -669,6 +671,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { peerId: peerId, peer: nil ) + call.sourcePanel = sourcePanel strongSelf.updateCurrentGroupCall(call) strongSelf.currentGroupCallPromise.set(.single(call)) strongSelf.hasActiveCallsPromise.set(true) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 2e2eebedb2..16f4595588 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AsyncDisplayKit import Postbox import TelegramCore import SyncCore @@ -256,6 +257,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var checkCallDisposable: Disposable? private var isCurrentlyConnecting: Bool? + public weak var sourcePanel: ASDisplayNode? + init( accountContext: AccountContext, audioSession: ManagedAudioSession, diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index 970cec0cea..f9fd11e989 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -6,6 +6,23 @@ import Display private let titleFont = Font.regular(17.0) private let subtitleFont = Font.regular(13.0) +private let blue = UIColor(rgb: 0x0078ff) +private let lightBlue = UIColor(rgb: 0x59c7f8) +private let green = UIColor(rgb: 0x33c659) + +private let deviceScale = UIScreen.main.scale + +private let radialMaskImage = generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + let colorSpace = CGColorSpaceCreateDeviceRGB() + var locations: [CGFloat] = [0.0, 1.0] + let maskColors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.75).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] + let maskGradient = CGGradient(colorsSpace: colorSpace, colors: maskColors as CFArray, locations: &locations)! + let maskGradientCenter = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + context.drawRadialGradient(maskGradient, startCenter: maskGradientCenter, startRadius: 0.0, endCenter: maskGradientCenter, endRadius: size.width / 2.0, options: .drawsAfterEndLocation) +}, opaque: false, scale: deviceScale)! + enum VoiceChatActionButtonState { enum ActiveState { case cantSpeak @@ -24,29 +41,51 @@ private enum VoiceChatActionButtonBackgroundNodeType { } private protocol VoiceChatActionButtonBackgroundNodeState: NSObjectProtocol { + var blueGradient: UIImage? { get set } + var greenGradient: UIImage? { get set } + + var frameInterval: Int { get } var isAnimating: Bool { get } var type: VoiceChatActionButtonBackgroundNodeType { get } func updateAnimations() } private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject, VoiceChatActionButtonBackgroundNodeState { + var blueGradient: UIImage? + var greenGradient: UIImage? + var isAnimating: Bool { return true } + var frameInterval: Int { + return 1 + } + var type: VoiceChatActionButtonBackgroundNodeType { return .connecting } func updateAnimations() { } + + init(blueGradient: UIImage?) { + self.blueGradient = blueGradient + } } private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState { + var blueGradient: UIImage? + var greenGradient: UIImage? + var isAnimating: Bool { return false } + var frameInterval: Int { + return 1 + } + var type: VoiceChatActionButtonBackgroundNodeType { return .disabled } @@ -105,7 +144,7 @@ private final class Blob { private var transitionArguments: (startTime: Double, duration: Double)? - var loop: Bool = false { + var loop: Bool = true { didSet { if let _ = transitionArguments { } else { @@ -267,10 +306,17 @@ private final class Blob { } private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, VoiceChatActionButtonBackgroundNodeState { + var blueGradient: UIImage? + var greenGradient: UIImage? + var isAnimating: Bool { return true } + var frameInterval: Int { + return 2 + } + var type: VoiceChatActionButtonBackgroundNodeType { return .blob } @@ -281,8 +327,10 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic var active: Bool var activeTransitionArguments: (startTime: Double, duration: Double)? - init(size: CGSize, active: Bool) { + init(size: CGSize, active: Bool, blueGradient: UIImage, greenGradient: UIImage) { self.active = active + self.blueGradient = blueGradient + self.greenGradient = greenGradient let mediumBlobRange: BlobRange = (0.69, 0.87) let bigBlobRange: BlobRange = (0.71, 1.00) @@ -302,6 +350,16 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic } func updateAnimations() { + let timestamp = CACurrentMediaTime() + + if let (startTime, duration) = self.activeTransitionArguments, duration > 0.0 { + let transition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration))) + if transition < 1.0 { + } else { + self.activeTransitionArguments = nil + } + } + for blob in self.blobs { blob.updateAnimations() } @@ -360,7 +418,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { private var animator: ConstantDisplayLinkAnimator? override init() { - self.state = VoiceChatActionButtonBackgroundNodeConnectingState() + self.state = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: nil) super.init() @@ -370,16 +428,19 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { func update(state: VoiceChatActionButtonBackgroundNodeState, animated: Bool) { var animated = animated + var hadState = true if !self.hasState { + hadState = false self.hasState = true animated = false } - if state.type != self.state.type { + if state.type != self.state.type || !hadState { if animated { self.transition = VoiceChatActionButtonBackgroundNodeTransition(startTime: CACurrentMediaTime(), duration: 0.3, previousState: self.state) } self.state = state + self.animator?.frameInterval = state.frameInterval } else if let blobState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState, let nextState = state as? VoiceChatActionButtonBackgroundNodeBlobState { blobState.update(with: nextState) } @@ -419,6 +480,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { animator = ConstantDisplayLinkAnimator(update: { [weak self] in self?.updateAnimations() }) + animator.frameInterval = 2 self.animator = animator } animator.isPaused = false @@ -435,6 +497,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! + + let drawStart = CACurrentMediaTime() if !isRasterizing { context.setBlendMode(.copy) @@ -450,80 +514,76 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { let buttonSize = CGSize(width: 144.0, height: 144.0) let radius = buttonSize.width / 2.0 - let blue = UIColor(rgb: 0x0078ff) - let lightBlue = UIColor(rgb: 0x59c7f8) - let green = UIColor(rgb: 0x33c659) - - var firstColor = lightBlue - var secondColor = blue - - var locations: [CGFloat] = [0.0, 1.0] - let colorSpace = CGColorSpaceCreateDeviceRGB() - var gradientCenter = CGPoint(x: bounds.size.width - 30.0, y: 50.0) - let gradientStartRadius: CGFloat = 0.0 - let gradientEndRadius: CGFloat = 260.0 + + var gradientTransition: CGFloat = 0.0 + var gradientImage: UIImage? = parameters.state.blueGradient + let gradientSize: CGFloat = bounds.width * 2.0 + + context.interpolationQuality = .low + + var appearanceProgress: CGFloat = 1.0 + if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState { + appearanceProgress = transition.progress(time: parameters.timestamp) + } if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState { - var gradientTransition: CGFloat = blobsState.active ? 1.0 : 0.0 + gradientTransition = blobsState.active ? 1.0 : 0.0 if let transition = blobsState.activeTransitionArguments { gradientTransition = CGFloat((parameters.timestamp - transition.startTime) / transition.duration) if !blobsState.active { gradientTransition = 1.0 - gradientTransition } } - - firstColor = firstColor.interpolateTo(blue, fraction: gradientTransition)! - secondColor = secondColor.interpolateTo(green, fraction: gradientTransition)! - - let maskGradientStartRadius: CGFloat = 0.0 - var maskGradientEndRadius: CGFloat = bounds.size.width / 2.0 - if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState { - maskGradientEndRadius *= transition.progress(time: parameters.timestamp) + gradientImage = gradientTransition.isZero ? blobsState.blueGradient : blobsState.greenGradient + if gradientTransition > 0.0 && gradientTransition < 1.0 { + gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in + context.interpolationQuality = .low + if let image = blobsState.blueGradient?.cgImage { + context.draw(image, in: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0))) + } + + context.setAlpha(gradientTransition) + if let image = blobsState.greenGradient?.cgImage { + context.draw(image, in: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0))) + } + }, opaque: true, scale: deviceScale)! } - - let maskGradientCenter = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) - let colors: [CGColor] = [secondColor.withAlphaComponent(0.5).cgColor, secondColor.withAlphaComponent(0.0).cgColor] - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - context.drawRadialGradient(gradient, startCenter: maskGradientCenter, startRadius: maskGradientStartRadius, endCenter: maskGradientCenter, endRadius: maskGradientEndRadius, options: .drawsAfterEndLocation) -// context.setBlendMode(.clear) -// -// -// let maskColors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor] -// let maskGradient = CGGradient(colorsSpace: colorSpace, colors: maskColors as CFArray, locations: &locations)! -// -// let maskGradientStartRadius: CGFloat = 0.0 -// let maskGradientEndRadius: CGFloat = bounds.size.width / 2.0 -//// context.drawRadialGradient(maskGradient, startCenter: maskGradientCenter, startRadius: maskGradientStartRadius, endCenter: maskGradientCenter, endRadius: maskGradientEndRadius, options: .drawsAfterEndLocation) -// -// context.setBlendMode(.normal) + context.saveGState() + var maskBounds = bounds + if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState { + let progress = 1.0 - appearanceProgress + maskBounds = maskBounds.insetBy(dx: bounds.width / 3.0 * progress, dy: bounds.width / 3.0 * progress) + } + context.clip(to: maskBounds, mask: radialMaskImage.cgImage!) + + if let gradient = gradientImage?.cgImage { + context.draw(gradient, in: CGRect(origin: CGPoint(x: gradientCenter.x - gradientSize / 2.0, y: gradientCenter.y - gradientSize / 2.0), size: CGSize(width: gradientSize, height: gradientSize))) + } + context.restoreGState() } - - - let colors: [CGColor] = [firstColor.cgColor, secondColor.cgColor] - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! -// center.x -= parameters.gradientMovement * 60.0 -// center.y += parameters.gradientMovement * 200.0 - context.saveGState() + if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState { for blob in blobsState.blobs { if let path = blob.currentShape, let uiPath = path.copy() as? UIBezierPath { let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0) - + uiPath.apply(toOrigin) - uiPath.apply(CGAffineTransform(scaleX: blob.currentScale, y: blob.currentScale)) + uiPath.apply(CGAffineTransform(scaleX: blob.currentScale * appearanceProgress, y: blob.currentScale * appearanceProgress)) uiPath.apply(fromOrigin) - + context.addPath(uiPath.cgPath) context.clip() - + context.setAlpha(blob.alpha) - - context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation) + + if let gradient = gradientImage?.cgImage { + context.draw(gradient, in: CGRect(origin: CGPoint(x: gradientCenter.x - gradientSize / 2.0, y: gradientCenter.y - gradientSize / 2.0), size: CGSize(width: gradientSize, height: gradientSize))) + } } } } @@ -598,8 +658,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { drawGradient = true } - if drawGradient { - context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation) + if drawGradient, let gradient = gradientImage?.cgImage { + context.draw(gradient, in: CGRect(origin: CGPoint(x: gradientCenter.x - gradientSize / 2.0, y: gradientCenter.y - gradientSize / 2.0), size: CGSize(width: gradientSize, height: gradientSize))) } if let clearInside = clearInside { @@ -616,6 +676,9 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { let titleLabel: ImmediateTextNode let subtitleLabel: ImmediateTextNode + let blueGradient: UIImage + let greenGradient: UIImage + private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButtonState, title: String, subtitle: String)? var pressing: Bool = false { @@ -638,6 +701,37 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.titleLabel = ImmediateTextNode() self.subtitleLabel = ImmediateTextNode() + self.blueGradient = generateImage(CGSize(width: 180.0, height: 180.0), contextGenerator: { size, context in + let firstColor = lightBlue + let secondColor = blue + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let gradientCenter = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + let gradientStartRadius: CGFloat = 0.0 + let gradientEndRadius: CGFloat = 85.0 + + let colors: [CGColor] = [firstColor.cgColor, secondColor.cgColor] + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation) + }, opaque: true, scale: min(2.0, deviceScale))! + + self.greenGradient = generateImage(CGSize(width: 180.0, height: 180.0), contextGenerator: { size, context in + let firstColor = blue + let secondColor = green + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let gradientCenter = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + let gradientStartRadius: CGFloat = 0.0 + let gradientEndRadius: CGFloat = 85.0 + + let colors: [CGColor] = [firstColor.cgColor, secondColor.cgColor] + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation) + }, opaque: true, scale: min(2.0, deviceScale))! super.init() @@ -677,6 +771,8 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white) self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white) + let blobSize: CGSize = CGSize(width: 244.0, height: 244.0) + var iconMuted = true var iconColor: UIColor = .white var backgroundState: VoiceChatActionButtonBackgroundNodeState @@ -685,17 +781,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { switch state { case .on: iconMuted = false - backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: size, active: true) + backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: blobSize, active: true, blueGradient: self.blueGradient, greenGradient: self.greenGradient) case .muted: - backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: size, active: false) + backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: blobSize, active: false, blueGradient: self.blueGradient, greenGradient: self.greenGradient) case .cantSpeak: iconColor = UIColor(rgb: 0xff3b30) backgroundState = VoiceChatActionButtonBackgroundNodeDisabledState() - default: - break } case .connecting: - backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState() + backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: self.blueGradient) } self.backgroundNode.update(state: backgroundState, animated: true) @@ -722,7 +816,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let totalHeight = titleSize.height + subtitleSize.height + 1.0 - self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height + 16.0 - totalHeight / 2.0) - 20.0), size: titleSize) + self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height + 16.0 - totalHeight / 2.0) - 56.0), size: titleSize) self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 5e9725a331..8ee35dcaa2 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -55,6 +55,12 @@ private final class VoiceChatControllerTitleView: UIView { self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor.white.withAlphaComponent(0.5)) } + func animateIn(duration: Double) { + self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 49.0), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.infoNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 49.0), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.titleNode.layer.animateScale(from: 0.882, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } + override func layoutSubviews() { super.layoutSubviews() @@ -377,33 +383,34 @@ public final class VoiceChatController: ViewController { }))) } default: - if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) { - if let muteState = entry.muteState, !muteState.canUnmute { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false) - f(.default) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - guard let strongSelf = self else { - return - } - - strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) - f(.default) - }))) - } - } - if peer.id != strongSelf.context.account.peerId { + if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) { + if let muteState = entry.muteState, !muteState.canUnmute { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + guard let strongSelf = self else { + return + } + + strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false) + f(.default) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + guard let strongSelf = self else { + return + } + + strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) + f(.default) + }))) + } + } + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] _, f in @@ -420,6 +427,8 @@ public final class VoiceChatController: ViewController { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() + + })) actionSheet.setItemGroups([ @@ -812,7 +821,7 @@ public final class VoiceChatController: ViewController { self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) let sideButtonSize = CGSize(width: 60.0, height: 60.0) - let centralButtonSize = CGSize(width: 244.0, height: 244.0) + let centralButtonSize = CGSize(width: 300.0, height: 300.0) let sideButtonInset: CGFloat = 27.0 let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize) @@ -898,7 +907,7 @@ public final class VoiceChatController: ViewController { soundImage = .speaker case .speaker: soundImage = .speaker - soundAppearance = .blurred(isFilled: true) +// soundAppearance = .blurred(isFilled: true) case .headphones: soundImage = .bluetooth case let .bluetooth(type): @@ -926,30 +935,116 @@ public final class VoiceChatController: ViewController { } } - func animateIn() { + func animateIn(sourcePanel: ASDisplayNode?) { self.alpha = 1.0 - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + guard let (layout, _) = self.validLayout else { + return + } + + if let sourcePanel = sourcePanel as? GroupCallNavigationAccessoryPanel { + let sourceFrame = sourcePanel.view.convert(sourcePanel.bounds, to: self.view) + self.contentContainer.clipsToBounds = true + + let duration: Double = 0.4 + if let titleView = self.controller?.navigationItem.titleView as? VoiceChatControllerTitleView { + titleView.animateIn(duration: duration) + + self.controller?.navigationBar?.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + + if let panelTitleView = sourcePanel.titleNode.view.snapshotContentTree() { + let frame = sourcePanel.titleNode.view.convert(sourcePanel.titleNode.bounds, to: self.view) + panelTitleView.frame = frame + self.view.addSubview(panelTitleView) - self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - - self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - - self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - - self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + panelTitleView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -49.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + panelTitleView.layer.animateScale(from: 1.0, to: 1.13, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + panelTitleView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak panelTitleView] _ in + panelTitleView?.removeFromSuperview() + }) + } + if let panelTextView = sourcePanel.textNode.view.snapshotContentTree() { + let frame = sourcePanel.textNode.view.convert(sourcePanel.textNode.bounds, to: self.view) + panelTextView.frame = frame + self.view.addSubview(panelTextView) + + panelTextView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -49.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + panelTextView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak panelTextView] _ in + panelTextView?.removeFromSuperview() + }) + } + } + + if let (backgroundView, foregroundView) = sourcePanel.rightButtonSnapshotViews() { + self.view.addSubview(backgroundView) + self.view.addSubview(foregroundView) + + self.optionsButton.isHidden = true + let optionsFrame = self.optionsButton.view.convert(self.optionsButton.bounds, to: self.view) + + let dotsView = UIImageView(image: optionsButtonImage()) + dotsView.center = foregroundView.center + self.view.addSubview(dotsView) + + backgroundView.layer.animateBounds(from: backgroundView.bounds, to: CGRect(origin: CGPoint(), size: CGSize(width: backgroundView.bounds.height, height: backgroundView.bounds.height)), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + backgroundView.layer.animatePosition(from: backgroundView.center, to: CGPoint(x: optionsFrame.midX, y: optionsFrame.midY), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + backgroundView.layer.animateScale(from: 1.0, to: optionsFrame.height / backgroundView.frame.height, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + foregroundView.layer.animatePosition(from: foregroundView.center, to: CGPoint(x: optionsFrame.midX, y: optionsFrame.midY), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + backgroundView.layer.animate(from: backgroundView.backgroundColor!.cgColor, to: UIColor(rgb: 0x1c1c1e).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration - 0.1, removeOnCompletion: false) + foregroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration - 0.1, removeOnCompletion: false, completion: { [weak self, weak foregroundView, weak backgroundView, weak dotsView] _ in + backgroundView?.removeFromSuperview() + foregroundView?.removeFromSuperview() + dotsView?.removeFromSuperview() + + self?.optionsButton.isHidden = false + }) + + foregroundView.layer.animateScale(from: 1.0, to: 0.3, duration: duration - 0.1, timingFunction: kCAMediaTimingFunctionSpring) + + dotsView.layer.animatePosition(from: dotsView.center, to: CGPoint(x: optionsFrame.midX, y: optionsFrame.midY), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + dotsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.1, removeOnCompletion: false) + } + + self.contentContainer.layer.animateFrame(from: sourceFrame, to: self.contentContainer.frame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.contentContainer.layer.animate(from: 0.0 as NSNumber, to: layout.deviceMetrics.screenCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: duration, removeOnCompletion: true, completion: { [weak self] value in + if value { + self?.contentContainer.clipsToBounds = false + } + }) + + self.contentContainer.layer.animate(from: self.presentationData.theme.rootController.navigationBar.backgroundColor.cgColor, to: UIColor.black.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration - 0.1) + + self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } else { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } } func animateOut(completion: (() -> Void)?) { self.alpha = 0.0 - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in + self.layer.allowsGroupOpacity = true + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] _ in completion?() + self?.layer.allowsGroupOpacity = false }) + self.contentContainer.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3) } private func enqueueTransition(_ transition: ListTransition) { @@ -1108,6 +1203,7 @@ public final class VoiceChatController: ViewController { initialBounds.origin = CGPoint() self?.bounds = initialBounds self?.controller?.statusBar.statusBarStyle = .White + self?.contentContainer.cornerRadius = 0.0 }) } default: @@ -1120,6 +1216,8 @@ public final class VoiceChatController: ViewController { public let call: PresentationGroupCall private let presentationData: PresentationData + public weak var sourcePanel: ASDisplayNode? + fileprivate let contentsReady = ValuePromise(false, ignoreRepeated: true) fileprivate let dataReady = ValuePromise(false, ignoreRepeated: true) private let _ready = Promise(false) @@ -1187,7 +1285,8 @@ public final class VoiceChatController: ViewController { if !self.didAppearOnce { self.didAppearOnce = true - self.controllerNode.animateIn() + self.controllerNode.animateIn(sourcePanel: self.sourcePanel) + self.sourcePanel = nil } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift index 57e4d59ca0..75e1b8403e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift @@ -151,18 +151,24 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { context.setFillColor(parameters.color.cgColor) + var lineWidth: CGFloat = 1.0 + UIScreenPixel if bounds.size.width > 36.0 { context.scaleBy(x: 2.5, y: 2.5) + } else if bounds.size.width < 30.0 { + context.scaleBy(x: 0.7, y: 0.7) + lineWidth = 2.0 } context.translateBy(x: 18.0, y: 18.0) let _ = try? drawSvgPath(context, path: "M-0.004000000189989805,-9.86400032043457 C2.2960000038146973,-9.86400032043457 4.165999889373779,-8.053999900817871 4.25600004196167,-5.77400016784668 C4.25600004196167,-5.77400016784668 4.265999794006348,-5.604000091552734 4.265999794006348,-5.604000091552734 C4.265999794006348,-5.604000091552734 4.265999794006348,-0.8040000200271606 4.265999794006348,-0.8040000200271606 C4.265999794006348,1.555999994277954 2.3559999465942383,3.4660000801086426 -0.004000000189989805,3.4660000801086426 C-2.2939999103546143,3.4660000801086426 -4.164000034332275,1.6460000276565552 -4.263999938964844,-0.6240000128746033 C-4.263999938964844,-0.6240000128746033 -4.263999938964844,-0.8040000200271606 -4.263999938964844,-0.8040000200271606 C-4.263999938964844,-0.8040000200271606 -4.263999938964844,-5.604000091552734 -4.263999938964844,-5.604000091552734 C-4.263999938964844,-7.953999996185303 -2.3540000915527344,-9.86400032043457 -0.004000000189989805,-9.86400032043457 Z ") - context.setBlendMode(.clear) - - let _ = try? drawSvgPath(context, path: "M0.004000000189989805,-8.53600025177002 C-1.565999984741211,-8.53600025177002 -2.8459999561309814,-7.306000232696533 -2.936000108718872,-5.75600004196167 C-2.936000108718872,-5.75600004196167 -2.936000108718872,-5.5960001945495605 -2.936000108718872,-5.5960001945495605 C-2.936000108718872,-5.5960001945495605 -2.936000108718872,-0.7960000038146973 -2.936000108718872,-0.7960000038146973 C-2.936000108718872,0.8240000009536743 -1.6260000467300415,2.134000062942505 0.004000000189989805,2.134000062942505 C1.5740000009536743,2.134000062942505 2.8540000915527344,0.9039999842643738 2.934000015258789,-0.6460000276565552 C2.934000015258789,-0.6460000276565552 2.934000015258789,-0.7960000038146973 2.934000015258789,-0.7960000038146973 C2.934000015258789,-0.7960000038146973 2.934000015258789,-5.5960001945495605 2.934000015258789,-5.5960001945495605 C2.934000015258789,-7.22599983215332 1.6239999532699585,-8.53600025177002 0.004000000189989805,-8.53600025177002 Z ") + if bounds.size.width > 30.0 { + context.setBlendMode(.clear) - context.setBlendMode(.normal) + let _ = try? drawSvgPath(context, path: "M0.004000000189989805,-8.53600025177002 C-1.565999984741211,-8.53600025177002 -2.8459999561309814,-7.306000232696533 -2.936000108718872,-5.75600004196167 C-2.936000108718872,-5.75600004196167 -2.936000108718872,-5.5960001945495605 -2.936000108718872,-5.5960001945495605 C-2.936000108718872,-5.5960001945495605 -2.936000108718872,-0.7960000038146973 -2.936000108718872,-0.7960000038146973 C-2.936000108718872,0.8240000009536743 -1.6260000467300415,2.134000062942505 0.004000000189989805,2.134000062942505 C1.5740000009536743,2.134000062942505 2.8540000915527344,0.9039999842643738 2.934000015258789,-0.6460000276565552 C2.934000015258789,-0.6460000276565552 2.934000015258789,-0.7960000038146973 2.934000015258789,-0.7960000038146973 C2.934000015258789,-0.7960000038146973 2.934000015258789,-5.5960001945495605 2.934000015258789,-5.5960001945495605 C2.934000015258789,-7.22599983215332 1.6239999532699585,-8.53600025177002 0.004000000189989805,-8.53600025177002 Z ") + + context.setBlendMode(.normal) + } let _ = try? drawSvgPath(context, path: "M6.796000003814697,-1.4639999866485596 C7.165999889373779,-1.4639999866485596 7.466000080108643,-1.1640000343322754 7.466000080108643,-0.8040000200271606 C7.466000080108643,3.0959999561309814 4.47599983215332,6.296000003814697 0.6660000085830688,6.636000156402588 C0.6660000085830688,6.636000156402588 0.6660000085830688,9.196000099182129 0.6660000085830688,9.196000099182129 C0.6660000085830688,9.565999984741211 0.3659999966621399,9.866000175476074 -0.004000000189989805,9.866000175476074 C-0.33399999141693115,9.866000175476074 -0.6140000224113464,9.605999946594238 -0.6539999842643738,9.28600025177002 C-0.6539999842643738,9.28600025177002 -0.6639999747276306,9.196000099182129 -0.6639999747276306,9.196000099182129 C-0.6639999747276306,9.196000099182129 -0.6639999747276306,6.636000156402588 -0.6639999747276306,6.636000156402588 C-4.473999977111816,6.296000003814697 -7.464000225067139,3.0959999561309814 -7.464000225067139,-0.8040000200271606 C-7.464000225067139,-1.1640000343322754 -7.164000034332275,-1.4639999866485596 -6.803999900817871,-1.4639999866485596 C-6.434000015258789,-1.4639999866485596 -6.133999824523926,-1.1640000343322754 -6.133999824523926,-0.8040000200271606 C-6.133999824523926,2.5859999656677246 -3.384000062942505,5.335999965667725 -0.004000000189989805,5.335999965667725 C3.385999917984009,5.335999965667725 6.136000156402588,2.5859999656677246 6.136000156402588,-0.8040000200271606 C6.136000156402588,-1.1640000343322754 6.435999870300293,-1.4639999866485596 6.796000003814697,-1.4639999866485596 Z ") @@ -188,7 +194,7 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { context.setBlendMode(.normal) context.setStrokeColor(parameters.color.cgColor) - context.setLineWidth(1.0 + UIScreenPixel) + context.setLineWidth(lineWidth) context.setLineCap(.round) context.setLineJoin(.round) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift index c87c88a8e6..f361dbf34b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift @@ -3,6 +3,17 @@ import UIKit import AsyncDisplayKit import Display +func optionsButtonImage() -> UIImage? { + return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(x: 6.0, y: 12.0, width: 4.0, height: 4.0)) + context.fillEllipse(in: CGRect(x: 12.0, y: 12.0, width: 4.0, height: 4.0)) + context.fillEllipse(in: CGRect(x: 18.0, y: 12.0, width: 4.0, height: 4.0)) + }) +} + final class VoiceChatOptionsButton: HighlightableButtonNode { let extractedContainerNode: ContextExtractedContentContainingNode let containerNode: ContextControllerSourceNode diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 4ad0064340..a05ac787e8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -271,6 +271,8 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) } + transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0) + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0)) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index dbeaad1624..a981abcb77 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1375,7 +1375,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peer.id { - context.sharedContext.navigateToCurrentCall() + context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in @@ -5986,10 +5986,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return } - let callResult = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id, initialCall: activeCall, endCurrentIfAny: false) + let callResult = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id, initialCall: activeCall, endCurrentIfAny: false, sourcePanel: nil) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peer.id { - strongSelf.context.sharedContext.navigateToCurrentCall() + strongSelf.context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in @@ -5999,7 +5999,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, let current = current { strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { if let strongSelf = self { - let _ = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id, initialCall: activeCall, endCurrentIfAny: true) + let _ = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id, initialCall: activeCall, endCurrentIfAny: true, sourcePanel: nil) } })]), in: .window(.root)) } else { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 179d2563e2..c0f359f47f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3169,7 +3169,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let cachedChannelData = self.data?.cachedData as? CachedChannelData else { return } - let _ = self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peer.id, initialCall: cachedChannelData.activeCall, endCurrentIfAny: false) + let _ = self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peer.id, initialCall: cachedChannelData.activeCall, endCurrentIfAny: false, sourcePanel: nil) return } @@ -3185,7 +3185,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let callResult = self.context.sharedContext.callManager?.requestCall(context: self.context, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peer.id { - self.context.sharedContext.navigateToCurrentCall() + self.context.sharedContext.navigateToCurrentCall(sourcePanel: nil) } else { let presentationData = self.presentationData let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e9b11b138d..0d794c0c9b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -28,11 +28,6 @@ import AlertUI import PresentationDataUtils import LocationUI -private enum CallStatusText: Equatable { - case none - case inProgress(Double?) -} - private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() let tokens = Bag() @@ -98,8 +93,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { private var callDisposable: Disposable? private var callStateDisposable: Disposable? - private var currentCallStatusText: CallStatusText = .none - private var currentCallStatusTextTimer: SwiftSignalKit.Timer? + + private var currentCallStatusBarNode: CallStatusBarNodeImpl? private var groupCallDisposable: Disposable? @@ -645,6 +640,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { if let call = call { mainWindow.hostView.containerView.endEditing(true) let groupCallController = VoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call) + groupCallController.sourcePanel = call.sourcePanel + call.sourcePanel = nil strongSelf.groupCallController = groupCallController strongSelf.mainWindow?.present(groupCallController, on: .calls) strongSelf.hasOngoingCall.set(true) @@ -655,54 +652,56 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) - self.callStateDisposable = combineLatest(queue: .mainQueue(), - self.callState.get(), - callManager.currentGroupCallSignal - |> map { call -> Bool in - return call != nil - } - ).start(next: { [weak self] state, hasGroupCall in - if let strongSelf = self { - let resolvedText: CallStatusText - if let state = state { - switch state.state { - case .connecting, .requesting, .terminating, .ringing, .waiting: - resolvedText = .inProgress(nil) - case .terminated: - resolvedText = .none - case .active(let timestamp, _, _), .reconnecting(let timestamp, _, _): - resolvedText = .inProgress(timestamp) + let callAndStateSignal: Signal<(PresentationCall, PresentationCallState)?, NoError> = .single(nil) + |> then( + callManager.currentCallSignal + |> mapToSignal { call in + if let call = call { + return call.state + |> map { state in + return (call, state) } - } else if hasGroupCall { - resolvedText = .inProgress(nil) } else { - resolvedText = .none + return .single(nil) + } + } + ) + let groupCallAndStateSignal: Signal = .single(nil) + |> then( + callManager.currentGroupCallSignal + ) + + self.callStateDisposable = combineLatest(queue: .mainQueue(), + callAndStateSignal, + groupCallAndStateSignal + ).start(next: { [weak self] callAndState, groupCall in + if let strongSelf = self { + let statusBarContent: CallStatusBarNodeImpl.Content? + + if let (call, state) = callAndState { + statusBarContent = .call(call) + } else if let groupCall = groupCall { + statusBarContent = .groupCall(groupCall) + } else { + statusBarContent = nil } - if strongSelf.currentCallStatusText != resolvedText { - strongSelf.currentCallStatusText = resolvedText - - var referenceTimestamp: Double? - if case let .inProgress(timestamp) = resolvedText, let concreteTimestamp = timestamp { - referenceTimestamp = concreteTimestamp - } - - if let _ = referenceTimestamp { - if strongSelf.currentCallStatusTextTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { - if let strongSelf = self { - strongSelf.updateStatusBarText() - } - }, queue: Queue.mainQueue()) - strongSelf.currentCallStatusTextTimer = timer - timer.start() - } + var resolvedCallStatusBarNode: CallStatusBarNodeImpl? + + if let statusBarContent = statusBarContent { + if let current = strongSelf.currentCallStatusBarNode { + resolvedCallStatusBarNode = current } else { - strongSelf.currentCallStatusTextTimer?.invalidate() - strongSelf.currentCallStatusTextTimer = nil + resolvedCallStatusBarNode = CallStatusBarNodeImpl() + strongSelf.currentCallStatusBarNode = resolvedCallStatusBarNode } - - strongSelf.updateStatusBarText() + resolvedCallStatusBarNode?.update(content: statusBarContent) + } else { + strongSelf.currentCallStatusBarNode = nil + } + + if let navigationController = strongSelf.mainWindow?.viewController as? NavigationController { + navigationController.setForceInCallStatusBar(resolvedCallStatusBarNode) } } }) @@ -780,7 +779,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() - self.currentCallStatusTextTimer?.invalidate() } private func updateAccountBackupData(account: Account) -> Signal { @@ -988,34 +986,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return openChatMessageImpl(params) } - private func updateStatusBarText() { - if case let .inProgress(timestamp) = self.currentCallStatusText { - let text: String - let presentationData = self.currentPresentationData.with { $0 } - if let timestamp = timestamp { - let duration = Int32(CFAbsoluteTimeGetCurrent() - timestamp) - let durationString: String - if duration > 60 * 60 { - durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60]) - } else { - durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60]) - } - - text = presentationData.strings.Call_StatusBar(durationString).0 - } else { - text = presentationData.strings.Call_StatusBar("").0 - } - if let navigationController = self.mainWindow?.viewController as? NavigationController { - navigationController.setForceInCallStatusBar(text) - } - } else { - if let navigationController = self.mainWindow?.viewController as? NavigationController { - navigationController.setForceInCallStatusBar(nil) - } - } - } - - public func navigateToCurrentCall() { + public func navigateToCurrentCall(sourcePanel: ASDisplayNode? = nil) { guard let mainWindow = self.mainWindow else { return } @@ -1026,6 +997,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } else if let groupCallController = self.groupCallController { if groupCallController.isNodeLoaded && groupCallController.view.superview == nil { + groupCallController.sourcePanel = sourcePanel mainWindow.hostView.containerView.endEditing(true) mainWindow.present(groupCallController, on: .calls) }