Voice Chat UI improvements

This commit is contained in:
Ilya Laktyushin 2020-11-28 00:47:16 +04:00
parent 2d890dd1e2
commit 2368dc4917
24 changed files with 520 additions and 346 deletions

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import SyncCore import SyncCore
@ -568,7 +569,7 @@ public protocol SharedAccountContext: class {
func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController
func navigateToCurrentCall() func navigateToCurrentCall(sourcePanel: ASDisplayNode?)
var hasOngoingCall: ValuePromise<Bool> { get } var hasOngoingCall: ValuePromise<Bool> { get }
var immediateHasOngoingCall: Bool { get } var immediateHasOngoingCall: Bool { get }

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import SyncCore import SyncCore
@ -246,6 +247,8 @@ public protocol PresentationGroupCall: class {
func invitePeer(_ peerId: PeerId) func invitePeer(_ peerId: PeerId)
var invitedPeers: Signal<Set<PeerId>, NoError> { get } var invitedPeers: Signal<Set<PeerId>, NoError> { get }
var sourcePanel: ASDisplayNode? { get set }
} }
public protocol PresentationCallManager: class { public protocol PresentationCallManager: class {
@ -253,5 +256,5 @@ public protocol PresentationCallManager: class {
var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get } var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get }
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult 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
} }

View File

@ -281,7 +281,7 @@ public final class CallListController: ViewController {
if case let .alreadyInProgress(currentPeerId) = callResult { if case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId { if currentPeerId == peerId {
began?() began?()
strongSelf.context.sharedContext.navigateToCurrentCall() strongSelf.context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = strongSelf.presentationData let presentationData = strongSelf.presentationData
let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in

View File

@ -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) let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: false)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId { if currentPeerId == peerId {
context.sharedContext.navigateToCurrentCall() context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in

View File

@ -391,12 +391,17 @@ open class NavigationController: UINavigationController, ContainableController,
} }
if let inCallStatusBar = self.inCallStatusBar { 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 { if inCallStatusBar.frame.isEmpty {
inCallStatusBar.frame = inCallStatusBarFrame inCallStatusBar.frame = inCallStatusBarFrame
} else { } else {
transition.updateFrame(node: inCallStatusBar, frame: inCallStatusBarFrame) transition.updateFrame(node: inCallStatusBar, frame: inCallStatusBarFrame)
} }
inCallStatusBar.callStatusBarNode?.update(size: inCallStatusBarFrame.size)
inCallStatusBar.callStatusBarNode?.frame = inCallStatusBarFrame
layout.statusBarHeight = inCallStatusBarFrame.height layout.statusBarHeight = inCallStatusBarFrame.height
self.inCallStatusBar?.frame = inCallStatusBarFrame 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)) { public func setForceInCallStatusBar(_ forceInCallStatusBar: CallStatusBarNode?, transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)) {
if let forceInCallStatusBarText = forceInCallStatusBarText { if let forceInCallStatusBar = forceInCallStatusBar {
let inCallStatusBar: StatusBar let inCallStatusBar: StatusBar
if let current = self.inCallStatusBar { if let current = self.inCallStatusBar {
inCallStatusBar = current inCallStatusBar = current
} else { } else {
inCallStatusBar = StatusBar() inCallStatusBar = StatusBar()
inCallStatusBar.clipsToBounds = false
inCallStatusBar.inCallNavigate = { [weak self] in inCallStatusBar.inCallNavigate = { [weak self] in
self?.scrollToTop(.master) self?.scrollToTop(.master)
} }
@ -1371,7 +1377,7 @@ open class NavigationController: UINavigationController, ContainableController,
} }
if let layout = self.validLayout { if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: transition) 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 { } else if let inCallStatusBar = self.inCallStatusBar {
self.inCallStatusBar = nil self.inCallStatusBar = nil

View File

@ -269,7 +269,7 @@ open class NavigationBar: ASDisplayNode {
private var title: String? { private var title: String? {
didSet { didSet {
if let title = self.title { 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 self.titleNode.accessibilityLabel = title
if self.titleNode.supernode == nil { if self.titleNode.supernode == nil {
self.buttonsContainerNode.addSubnode(self.titleNode) self.buttonsContainerNode.addSubnode(self.titleNode)

View File

@ -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) { private func addInCallAnimation(_ layer: CALayer) {
let animation = CAKeyframeAnimation(keyPath: "opacity") let animation = CAKeyframeAnimation(keyPath: "opacity")
@ -92,10 +96,7 @@ public final class StatusBar: ASDisplayNode {
private var removeProxyNodeScheduled = false private var removeProxyNodeScheduled = false
let offsetNode = ASDisplayNode() let offsetNode = ASDisplayNode()
private let inCallBackgroundNode = ASDisplayNode() var callStatusBarNode: CallStatusBarNode? = nil
private let inCallLabel: StatusBarLabelNode
private var inCallText: String? = nil
public var verticalOffset: CGFloat = 0.0 { public var verticalOffset: CGFloat = 0.0 {
didSet { didSet {
@ -113,14 +114,8 @@ public final class StatusBar: ASDisplayNode {
} }
public override init() { public override init() {
self.inCallLabel = StatusBarLabelNode()
self.inCallLabel.isUserInteractionEnabled = false
self.offsetNode.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() super.init()
self.setViewBlock({ self.setViewBlock({
@ -130,18 +125,17 @@ public final class StatusBar: ASDisplayNode {
(self.view as! StatusBarView).node = self (self.view as! StatusBarView).node = self
self.addSubnode(self.offsetNode) self.addSubnode(self.offsetNode)
self.addSubnode(self.inCallBackgroundNode)
self.clipsToBounds = true self.clipsToBounds = true
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) 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 { if let statusBar = statusBar {
self.removeProxyNodeScheduled = false self.removeProxyNodeScheduled = false
let resolvedStyle: StatusBarStyle let resolvedStyle: StatusBarStyle
if inCallText != nil && !self.ignoreInCall { if inCallNode != nil && !self.ignoreInCall {
resolvedStyle = .White resolvedStyle = .White
} else { } else {
resolvedStyle = self.statusBarStyle resolvedStyle = self.statusBarStyle
@ -176,46 +170,35 @@ public final class StatusBar: ASDisplayNode {
ignoreInCall = true ignoreInCall = true
} }
var resolvedInCallText: String? = inCallText var resolvedCallStatusBarNode: CallStatusBarNode? = inCallNode
if ignoreInCall { if ignoreInCall {
resolvedInCallText = nil resolvedCallStatusBarNode = nil
} }
if (resolvedInCallText != nil) != (self.inCallText != nil) { if (resolvedCallStatusBarNode != nil) != (self.callStatusBarNode != nil) {
if let _ = resolvedInCallText { if let resolvedCallStatusBarNode = resolvedCallStatusBarNode {
if !withSafeInsets { self.addSubnode(resolvedCallStatusBarNode)
self.addSubnode(self.inCallLabel)
}
addInCallAnimation(self.inCallLabel.layer)
self.inCallBackgroundNode.layer.backgroundColor = inCallBackgroundColor.cgColor
if animated { 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 { } else if let callStatusBarNode = self.callStatusBarNode {
self.inCallLabel.removeFromSupernode() self.callStatusBarNode = nil
self.inCallBackgroundNode.layer.backgroundColor = UIColor.clear.cgColor
if animated { 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()
} }
} }
} }
self.callStatusBarNode = resolvedCallStatusBarNode
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
} }
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 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 return self.view
} else { } else {
return nil return nil
@ -223,42 +206,8 @@ public final class StatusBar: ASDisplayNode {
} }
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, self.inCallText != nil { if case .ended = recognizer.state, self.callStatusBarNode != nil {
self.inCallNavigate?() 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)
}
}
}
} }

View File

@ -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) 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == user.id { if currentPeerId == user.id {
context.sharedContext.navigateToCurrentCall() context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in

View File

@ -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) 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peer.id { if currentPeerId == peer.id {
context.sharedContext.navigateToCurrentCall() context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in

View File

@ -21,7 +21,6 @@ swift_library(
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/Markdown:Markdown", "//submodules/Markdown:Markdown",
"//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -12,6 +12,7 @@ import UniversalMediaPlayer
import AccountContext import AccountContext
import OverlayStatusController import OverlayStatusController
import PresentationDataUtils import PresentationDataUtils
import TelegramCallsUI
public enum MediaAccessoryPanelVisibility { public enum MediaAccessoryPanelVisibility {
case none case none
@ -25,34 +26,6 @@ public enum LocationBroadcastPanelSource {
case peer(PeerId) 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) { private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) {
let presentImpl: (Message?) -> Void = { [weak controller] message in let presentImpl: (Message?) -> Void = { [weak controller] message in
if let message = message, let strongController = controller { if let message = message, let strongController = controller {
@ -430,7 +403,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
} }
strongSelf.joinGroupCall( strongSelf.joinGroupCall(
peerId: groupCallPanelData.peerId, peerId: groupCallPanelData.peerId,
info: groupCallPanelData.info info: groupCallPanelData.info,
sourcePanel: strongSelf.groupCallAccessoryPanel
) )
}) })
if let navigationBar = self.navigationBar { if let navigationBar = self.navigationBar {
@ -847,11 +821,11 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
})] })]
} }
private func joinGroupCall(peerId: PeerId, info: GroupCallInfo) { 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) 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId { if currentPeerId == peerId {
self.context.sharedContext.navigateToCurrentCall() self.context.sharedContext.navigateToCurrentCall(sourcePanel: sourcePanel)
} else { } else {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
@ -867,7 +841,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
if let current = current { 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: { 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 { 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)) })]), in: .window(.root))
} else { } else {

View File

@ -34,6 +34,7 @@ swift_library(
"//submodules/AnimationUI:AnimationUI", "//submodules/AnimationUI:AnimationUI",
"//submodules/UndoUI:UndoUI", "//submodules/UndoUI:UndoUI",
"//submodules/AudioBlob:AudioBlob", "//submodules/AudioBlob:AudioBlob",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1238,7 +1238,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
if !self.shouldStayHiddenUntilConnection || self.containerNode.alpha > 0.0 { if !self.shouldStayHiddenUntilConnection || self.containerNode.alpha > 0.0 {
self.containerNode.layer.allowsGroupOpacity = true 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.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 self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in
completion() completion()

View File

@ -15,7 +15,35 @@ import AnimatedAvatarSetNode
private let titleFont = Font.semibold(15.0) private let titleFont = Font.semibold(15.0)
private let subtitleFont = Font.regular(13.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 let context: AccountContext
private var theme: PresentationTheme private var theme: PresentationTheme
private var strings: PresentationStrings private var strings: PresentationStrings
@ -31,12 +59,11 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private let joinButtonBackgroundNode: ASImageNode private let joinButtonBackgroundNode: ASImageNode
private let micButton: HighlightTrackingButtonNode private let micButton: HighlightTrackingButtonNode
private let micButtonForegroundMutedNode: ASImageNode private let micButtonForegroundNode: VoiceChatMicrophoneNode
private let micButtonForegroundUnmutedNode: ASImageNode
private let micButtonBackgroundNode: ASImageNode private let micButtonBackgroundNode: ASImageNode
private let titleNode: ImmediateTextNode let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode let textNode: ImmediateTextNode
private let muteIconNode: ASImageNode private let muteIconNode: ASImageNode
private let avatarsContext: AnimatedAvatarSetContext private let avatarsContext: AnimatedAvatarSetContext
@ -51,7 +78,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private var currentData: GroupCallPanelData? private var currentData: GroupCallPanelData?
private var validLayout: (CGSize, CGFloat, CGFloat)? 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.context = context
self.theme = presentationData.theme self.theme = presentationData.theme
self.strings = presentationData.strings self.strings = presentationData.strings
@ -67,8 +94,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.joinButtonBackgroundNode = ASImageNode() self.joinButtonBackgroundNode = ASImageNode()
self.micButton = HighlightTrackingButtonNode() self.micButton = HighlightTrackingButtonNode()
self.micButtonForegroundMutedNode = ASImageNode() self.micButtonForegroundNode = VoiceChatMicrophoneNode()
self.micButtonForegroundUnmutedNode = ASImageNode()
self.micButtonBackgroundNode = ASImageNode() self.micButtonBackgroundNode = ASImageNode()
self.titleNode = ImmediateTextNode() self.titleNode = ImmediateTextNode()
@ -117,8 +143,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.joinButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.joinButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
self.micButton.addSubnode(self.micButtonBackgroundNode) self.micButton.addSubnode(self.micButtonBackgroundNode)
self.micButton.addSubnode(self.micButtonForegroundMutedNode) self.micButton.addSubnode(self.micButtonForegroundNode)
self.micButton.addSubnode(self.micButtonForegroundUnmutedNode)
self.contentNode.addSubnode(self.micButton) self.contentNode.addSubnode(self.micButton)
self.micButton.addTarget(self, action: #selector(self.micTapped), forControlEvents: [.touchUpInside]) self.micButton.addTarget(self, action: #selector(self.micTapped), forControlEvents: [.touchUpInside])
@ -132,7 +157,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.isMutedDisposable.dispose() self.isMutedDisposable.dispose()
} }
override func didLoad() { public override func didLoad() {
super.didLoad() super.didLoad()
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.micButtonPressGesture(_:))) 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) self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor)
//TODO:localize //TODO:localize
self.micButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: UIColor(rgb: 0x30B251)) 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)
//TODO:localize //TODO:localize
self.titleNode.attributedText = NSAttributedString(string: "Voice Chat", font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.primaryTextColor) 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) self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme)
} }
func update(data: GroupCallPanelData) { public func update(data: GroupCallPanelData) {
let previousData = self.currentData let previousData = self.currentData
self.currentData = data self.currentData = data
@ -244,8 +267,7 @@ final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.micButtonForegroundMutedNode.isHidden = !isMuted strongSelf.micButtonForegroundNode.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, color: UIColor.white), animated: true)
strongSelf.micButtonForegroundUnmutedNode.isHidden = isMuted
})) }))
} }
} else if data.groupCall == nil { } 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) self.validLayout = (size, leftInset, rightInset)
let panelHeight = size.height 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) 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.micButton, frame: micButtonFrame)
transition.updateFrame(node: self.micButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: micButtonFrame.size)) 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)) 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))
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 titleSize = self.titleNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let titleSize = self.titleNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let textSize = self.textNode.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.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)) 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))) 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 self.clipsToBounds = true
let contentPosition = self.contentNode.layer.position 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 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 self.clipsToBounds = true
let contentPosition = self.contentNode.layer.position 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 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() 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
}
} }

View File

@ -1,4 +1,5 @@
import Foundation import Foundation
import AsyncDisplayKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import SyncCore 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 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 let currentGroupCall = self.currentGroupCallValue {
if endCurrentIfAny { if endCurrentIfAny {
@ -618,7 +619,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
accountContext: AccountContext, accountContext: AccountContext,
peerId: PeerId, peerId: PeerId,
initialCall: CachedChannelData.ActiveCall?, initialCall: CachedChannelData.ActiveCall?,
internalId: CallSessionInternalId = CallSessionInternalId() internalId: CallSessionInternalId = CallSessionInternalId(),
sourcePanel: ASDisplayNode?
) -> Signal<Bool, NoError> { ) -> Signal<Bool, NoError> {
let (presentationData, present, openSettings) = self.getDeviceAccessData() let (presentationData, present, openSettings) = self.getDeviceAccessData()
@ -669,6 +671,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
peerId: peerId, peerId: peerId,
peer: nil peer: nil
) )
call.sourcePanel = sourcePanel
strongSelf.updateCurrentGroupCall(call) strongSelf.updateCurrentGroupCall(call)
strongSelf.currentGroupCallPromise.set(.single(call)) strongSelf.currentGroupCallPromise.set(.single(call))
strongSelf.hasActiveCallsPromise.set(true) strongSelf.hasActiveCallsPromise.set(true)

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import SyncCore import SyncCore
@ -256,6 +257,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var checkCallDisposable: Disposable? private var checkCallDisposable: Disposable?
private var isCurrentlyConnecting: Bool? private var isCurrentlyConnecting: Bool?
public weak var sourcePanel: ASDisplayNode?
init( init(
accountContext: AccountContext, accountContext: AccountContext,
audioSession: ManagedAudioSession, audioSession: ManagedAudioSession,

View File

@ -6,6 +6,23 @@ import Display
private let titleFont = Font.regular(17.0) private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.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 VoiceChatActionButtonState {
enum ActiveState { enum ActiveState {
case cantSpeak case cantSpeak
@ -24,29 +41,51 @@ private enum VoiceChatActionButtonBackgroundNodeType {
} }
private protocol VoiceChatActionButtonBackgroundNodeState: NSObjectProtocol { private protocol VoiceChatActionButtonBackgroundNodeState: NSObjectProtocol {
var blueGradient: UIImage? { get set }
var greenGradient: UIImage? { get set }
var frameInterval: Int { get }
var isAnimating: Bool { get } var isAnimating: Bool { get }
var type: VoiceChatActionButtonBackgroundNodeType { get } var type: VoiceChatActionButtonBackgroundNodeType { get }
func updateAnimations() func updateAnimations()
} }
private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject, VoiceChatActionButtonBackgroundNodeState { private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var blueGradient: UIImage?
var greenGradient: UIImage?
var isAnimating: Bool { var isAnimating: Bool {
return true return true
} }
var frameInterval: Int {
return 1
}
var type: VoiceChatActionButtonBackgroundNodeType { var type: VoiceChatActionButtonBackgroundNodeType {
return .connecting return .connecting
} }
func updateAnimations() { func updateAnimations() {
} }
init(blueGradient: UIImage?) {
self.blueGradient = blueGradient
}
} }
private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState { private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var blueGradient: UIImage?
var greenGradient: UIImage?
var isAnimating: Bool { var isAnimating: Bool {
return false return false
} }
var frameInterval: Int {
return 1
}
var type: VoiceChatActionButtonBackgroundNodeType { var type: VoiceChatActionButtonBackgroundNodeType {
return .disabled return .disabled
} }
@ -105,7 +144,7 @@ private final class Blob {
private var transitionArguments: (startTime: Double, duration: Double)? private var transitionArguments: (startTime: Double, duration: Double)?
var loop: Bool = false { var loop: Bool = true {
didSet { didSet {
if let _ = transitionArguments { if let _ = transitionArguments {
} else { } else {
@ -267,10 +306,17 @@ private final class Blob {
} }
private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, VoiceChatActionButtonBackgroundNodeState { private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var blueGradient: UIImage?
var greenGradient: UIImage?
var isAnimating: Bool { var isAnimating: Bool {
return true return true
} }
var frameInterval: Int {
return 2
}
var type: VoiceChatActionButtonBackgroundNodeType { var type: VoiceChatActionButtonBackgroundNodeType {
return .blob return .blob
} }
@ -281,8 +327,10 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
var active: Bool var active: Bool
var activeTransitionArguments: (startTime: Double, duration: Double)? var activeTransitionArguments: (startTime: Double, duration: Double)?
init(size: CGSize, active: Bool) { init(size: CGSize, active: Bool, blueGradient: UIImage, greenGradient: UIImage) {
self.active = active self.active = active
self.blueGradient = blueGradient
self.greenGradient = greenGradient
let mediumBlobRange: BlobRange = (0.69, 0.87) let mediumBlobRange: BlobRange = (0.69, 0.87)
let bigBlobRange: BlobRange = (0.71, 1.00) let bigBlobRange: BlobRange = (0.71, 1.00)
@ -302,6 +350,16 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
} }
func updateAnimations() { 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 { for blob in self.blobs {
blob.updateAnimations() blob.updateAnimations()
} }
@ -360,7 +418,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
private var animator: ConstantDisplayLinkAnimator? private var animator: ConstantDisplayLinkAnimator?
override init() { override init() {
self.state = VoiceChatActionButtonBackgroundNodeConnectingState() self.state = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: nil)
super.init() super.init()
@ -370,16 +428,19 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
func update(state: VoiceChatActionButtonBackgroundNodeState, animated: Bool) { func update(state: VoiceChatActionButtonBackgroundNodeState, animated: Bool) {
var animated = animated var animated = animated
var hadState = true
if !self.hasState { if !self.hasState {
hadState = false
self.hasState = true self.hasState = true
animated = false animated = false
} }
if state.type != self.state.type { if state.type != self.state.type || !hadState {
if animated { if animated {
self.transition = VoiceChatActionButtonBackgroundNodeTransition(startTime: CACurrentMediaTime(), duration: 0.3, previousState: self.state) self.transition = VoiceChatActionButtonBackgroundNodeTransition(startTime: CACurrentMediaTime(), duration: 0.3, previousState: self.state)
} }
self.state = state self.state = state
self.animator?.frameInterval = state.frameInterval
} else if let blobState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState, let nextState = state as? VoiceChatActionButtonBackgroundNodeBlobState { } else if let blobState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState, let nextState = state as? VoiceChatActionButtonBackgroundNodeBlobState {
blobState.update(with: nextState) blobState.update(with: nextState)
} }
@ -419,6 +480,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations() self?.updateAnimations()
}) })
animator.frameInterval = 2
self.animator = animator self.animator = animator
} }
animator.isPaused = false 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) { @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()! let context = UIGraphicsGetCurrentContext()!
let drawStart = CACurrentMediaTime()
if !isRasterizing { if !isRasterizing {
context.setBlendMode(.copy) context.setBlendMode(.copy)
@ -450,80 +514,76 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
let buttonSize = CGSize(width: 144.0, height: 144.0) let buttonSize = CGSize(width: 144.0, height: 144.0)
let radius = buttonSize.width / 2.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) 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 { 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 { if let transition = blobsState.activeTransitionArguments {
gradientTransition = CGFloat((parameters.timestamp - transition.startTime) / transition.duration) gradientTransition = CGFloat((parameters.timestamp - transition.startTime) / transition.duration)
if !blobsState.active { if !blobsState.active {
gradientTransition = 1.0 - gradientTransition gradientTransition = 1.0 - gradientTransition
} }
} }
gradientImage = gradientTransition.isZero ? blobsState.blueGradient : blobsState.greenGradient
firstColor = firstColor.interpolateTo(blue, fraction: gradientTransition)! if gradientTransition > 0.0 && gradientTransition < 1.0 {
secondColor = secondColor.interpolateTo(green, fraction: gradientTransition)! gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in
context.interpolationQuality = .low
let maskGradientStartRadius: CGFloat = 0.0 if let image = blobsState.blueGradient?.cgImage {
var maskGradientEndRadius: CGFloat = bounds.size.width / 2.0 context.draw(image, in: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0)))
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState { }
maskGradientEndRadius *= transition.progress(time: parameters.timestamp)
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) context.saveGState()
// var maskBounds = bounds
// if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState {
// let maskColors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor] let progress = 1.0 - appearanceProgress
// let maskGradient = CGGradient(colorsSpace: colorSpace, colors: maskColors as CFArray, locations: &locations)! maskBounds = maskBounds.insetBy(dx: bounds.width / 3.0 * progress, dy: bounds.width / 3.0 * progress)
// }
// let maskGradientStartRadius: CGFloat = 0.0 context.clip(to: maskBounds, mask: radialMaskImage.cgImage!)
// let maskGradientEndRadius: CGFloat = bounds.size.width / 2.0
//// context.drawRadialGradient(maskGradient, startCenter: maskGradientCenter, startRadius: maskGradientStartRadius, endCenter: maskGradientCenter, endRadius: maskGradientEndRadius, 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)))
// context.setBlendMode(.normal) }
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() context.saveGState()
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState { if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
for blob in blobsState.blobs { for blob in blobsState.blobs {
if let path = blob.currentShape, let uiPath = path.copy() as? UIBezierPath { 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 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) let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
uiPath.apply(toOrigin) 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) uiPath.apply(fromOrigin)
context.addPath(uiPath.cgPath) context.addPath(uiPath.cgPath)
context.clip() context.clip()
context.setAlpha(blob.alpha) 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 drawGradient = true
} }
if drawGradient { if drawGradient, let gradient = gradientImage?.cgImage {
context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation) 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 { if let clearInside = clearInside {
@ -616,6 +676,9 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
let titleLabel: ImmediateTextNode let titleLabel: ImmediateTextNode
let subtitleLabel: ImmediateTextNode let subtitleLabel: ImmediateTextNode
let blueGradient: UIImage
let greenGradient: UIImage
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButtonState, title: String, subtitle: String)? private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButtonState, title: String, subtitle: String)?
var pressing: Bool = false { var pressing: Bool = false {
@ -638,6 +701,37 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.titleLabel = ImmediateTextNode() self.titleLabel = ImmediateTextNode()
self.subtitleLabel = 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() super.init()
@ -677,6 +771,8 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white) self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white)
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: subtitleFont, 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 iconMuted = true
var iconColor: UIColor = .white var iconColor: UIColor = .white
var backgroundState: VoiceChatActionButtonBackgroundNodeState var backgroundState: VoiceChatActionButtonBackgroundNodeState
@ -685,17 +781,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
switch state { switch state {
case .on: case .on:
iconMuted = false iconMuted = false
backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: size, active: true) backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: blobSize, active: true, blueGradient: self.blueGradient, greenGradient: self.greenGradient)
case .muted: case .muted:
backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: size, active: false) backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: blobSize, active: false, blueGradient: self.blueGradient, greenGradient: self.greenGradient)
case .cantSpeak: case .cantSpeak:
iconColor = UIColor(rgb: 0xff3b30) iconColor = UIColor(rgb: 0xff3b30)
backgroundState = VoiceChatActionButtonBackgroundNodeDisabledState() backgroundState = VoiceChatActionButtonBackgroundNodeDisabledState()
default:
break
} }
case .connecting: case .connecting:
backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState() backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: self.blueGradient)
} }
self.backgroundNode.update(state: backgroundState, animated: true) 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 subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0 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.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) self.containerNode.frame = CGRect(origin: CGPoint(), size: size)

View File

@ -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)) 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() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
@ -377,33 +383,34 @@ public final class VoiceChatController: ViewController {
}))) })))
} }
default: 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 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 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] _, f in }, 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 items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated() actionSheet?.dismissAnimated()
})) }))
actionSheet.setItemGroups([ 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 }) 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 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 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) 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 soundImage = .speaker
case .speaker: case .speaker:
soundImage = .speaker soundImage = .speaker
soundAppearance = .blurred(isFilled: true) // soundAppearance = .blurred(isFilled: true)
case .headphones: case .headphones:
soundImage = .bluetooth soundImage = .bluetooth
case let .bluetooth(type): case let .bluetooth(type):
@ -926,30 +935,116 @@ public final class VoiceChatController: ViewController {
} }
} }
func animateIn() { func animateIn(sourcePanel: ASDisplayNode?) {
self.alpha = 1.0 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) 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)
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) panelTitleView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak panelTitleView] _ in
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) panelTitleView?.removeFromSuperview()
})
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) if let panelTextView = sourcePanel.textNode.view.snapshotContentTree() {
self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let frame = sourcePanel.textNode.view.convert(sourcePanel.textNode.bounds, to: self.view)
self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) panelTextView.frame = frame
self.view.addSubview(panelTextView)
self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
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)?) { func animateOut(completion: (() -> Void)?) {
self.alpha = 0.0 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?() completion?()
self?.layer.allowsGroupOpacity = false
}) })
self.contentContainer.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3)
} }
private func enqueueTransition(_ transition: ListTransition) { private func enqueueTransition(_ transition: ListTransition) {
@ -1108,6 +1203,7 @@ public final class VoiceChatController: ViewController {
initialBounds.origin = CGPoint() initialBounds.origin = CGPoint()
self?.bounds = initialBounds self?.bounds = initialBounds
self?.controller?.statusBar.statusBarStyle = .White self?.controller?.statusBar.statusBarStyle = .White
self?.contentContainer.cornerRadius = 0.0
}) })
} }
default: default:
@ -1120,6 +1216,8 @@ public final class VoiceChatController: ViewController {
public let call: PresentationGroupCall public let call: PresentationGroupCall
private let presentationData: PresentationData private let presentationData: PresentationData
public weak var sourcePanel: ASDisplayNode?
fileprivate let contentsReady = ValuePromise<Bool>(false, ignoreRepeated: true) fileprivate let contentsReady = ValuePromise<Bool>(false, ignoreRepeated: true)
fileprivate let dataReady = ValuePromise<Bool>(false, ignoreRepeated: true) fileprivate let dataReady = ValuePromise<Bool>(false, ignoreRepeated: true)
private let _ready = Promise<Bool>(false) private let _ready = Promise<Bool>(false)
@ -1187,7 +1285,8 @@ public final class VoiceChatController: ViewController {
if !self.didAppearOnce { if !self.didAppearOnce {
self.didAppearOnce = true self.didAppearOnce = true
self.controllerNode.animateIn() self.controllerNode.animateIn(sourcePanel: self.sourcePanel)
self.sourcePanel = nil
} }
} }

View File

@ -151,18 +151,24 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
context.setFillColor(parameters.color.cgColor) context.setFillColor(parameters.color.cgColor)
var lineWidth: CGFloat = 1.0 + UIScreenPixel
if bounds.size.width > 36.0 { if bounds.size.width > 36.0 {
context.scaleBy(x: 2.5, y: 2.5) 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) 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 ") 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) if bounds.size.width > 30.0 {
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 ")
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 ") 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.setBlendMode(.normal)
context.setStrokeColor(parameters.color.cgColor) context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(1.0 + UIScreenPixel) context.setLineWidth(lineWidth)
context.setLineCap(.round) context.setLineCap(.round)
context.setLineJoin(.round) context.setLineJoin(.round)

View File

@ -3,6 +3,17 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display 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 { final class VoiceChatOptionsButton: HighlightableButtonNode {
let extractedContainerNode: ContextExtractedContentContainingNode let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode let containerNode: ContextControllerSourceNode

View File

@ -271,6 +271,8 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) 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.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)) transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0))

View File

@ -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) 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peer.id { if currentPeerId == peer.id {
context.sharedContext.navigateToCurrentCall() context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in 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 { guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peer.id { if currentPeerId == peer.id {
strongSelf.context.sharedContext.navigateToCurrentCall() strongSelf.context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in 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 { 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: { 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 { 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)) })]), in: .window(.root))
} else { } else {

View File

@ -3169,7 +3169,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
guard let cachedChannelData = self.data?.cachedData as? CachedChannelData else { guard let cachedChannelData = self.data?.cachedData as? CachedChannelData else {
return 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 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) 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 let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peer.id { if currentPeerId == peer.id {
self.context.sharedContext.navigateToCurrentCall() self.context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else { } else {
let presentationData = self.presentationData let presentationData = self.presentationData
let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in

View File

@ -28,11 +28,6 @@ import AlertUI
import PresentationDataUtils import PresentationDataUtils
import LocationUI import LocationUI
private enum CallStatusText: Equatable {
case none
case inProgress(Double?)
}
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() let subscribers = Bag<(Bool) -> Void>()
let tokens = Bag<Void>() let tokens = Bag<Void>()
@ -98,8 +93,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private var callDisposable: Disposable? private var callDisposable: Disposable?
private var callStateDisposable: Disposable? private var callStateDisposable: Disposable?
private var currentCallStatusText: CallStatusText = .none
private var currentCallStatusTextTimer: SwiftSignalKit.Timer? private var currentCallStatusBarNode: CallStatusBarNodeImpl?
private var groupCallDisposable: Disposable? private var groupCallDisposable: Disposable?
@ -645,6 +640,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
if let call = call { if let call = call {
mainWindow.hostView.containerView.endEditing(true) mainWindow.hostView.containerView.endEditing(true)
let groupCallController = VoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call) let groupCallController = VoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
groupCallController.sourcePanel = call.sourcePanel
call.sourcePanel = nil
strongSelf.groupCallController = groupCallController strongSelf.groupCallController = groupCallController
strongSelf.mainWindow?.present(groupCallController, on: .calls) strongSelf.mainWindow?.present(groupCallController, on: .calls)
strongSelf.hasOngoingCall.set(true) strongSelf.hasOngoingCall.set(true)
@ -655,54 +652,56 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} }
}) })
self.callStateDisposable = combineLatest(queue: .mainQueue(), let callAndStateSignal: Signal<(PresentationCall, PresentationCallState)?, NoError> = .single(nil)
self.callState.get(), |> then(
callManager.currentGroupCallSignal callManager.currentCallSignal
|> map { call -> Bool in |> mapToSignal { call in
return call != nil if let call = call {
} return call.state
).start(next: { [weak self] state, hasGroupCall in |> map { state in
if let strongSelf = self { return (call, state)
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)
} }
} else if hasGroupCall {
resolvedText = .inProgress(nil)
} else { } else {
resolvedText = .none return .single(nil)
}
}
)
let groupCallAndStateSignal: Signal<PresentationGroupCall?, NoError> = .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 { var resolvedCallStatusBarNode: CallStatusBarNodeImpl?
strongSelf.currentCallStatusText = resolvedText
if let statusBarContent = statusBarContent {
var referenceTimestamp: Double? if let current = strongSelf.currentCallStatusBarNode {
if case let .inProgress(timestamp) = resolvedText, let concreteTimestamp = timestamp { resolvedCallStatusBarNode = current
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()
}
} else { } else {
strongSelf.currentCallStatusTextTimer?.invalidate() resolvedCallStatusBarNode = CallStatusBarNodeImpl()
strongSelf.currentCallStatusTextTimer = nil strongSelf.currentCallStatusBarNode = resolvedCallStatusBarNode
} }
resolvedCallStatusBarNode?.update(content: statusBarContent)
strongSelf.updateStatusBarText() } 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.callDisposable?.dispose()
self.groupCallDisposable?.dispose() self.groupCallDisposable?.dispose()
self.callStateDisposable?.dispose() self.callStateDisposable?.dispose()
self.currentCallStatusTextTimer?.invalidate()
} }
private func updateAccountBackupData(account: Account) -> Signal<Never, NoError> { private func updateAccountBackupData(account: Account) -> Signal<Never, NoError> {
@ -988,34 +986,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return openChatMessageImpl(params) return openChatMessageImpl(params)
} }
private func updateStatusBarText() { public func navigateToCurrentCall(sourcePanel: ASDisplayNode? = nil) {
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() {
guard let mainWindow = self.mainWindow else { guard let mainWindow = self.mainWindow else {
return return
} }
@ -1026,6 +997,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} }
} else if let groupCallController = self.groupCallController { } else if let groupCallController = self.groupCallController {
if groupCallController.isNodeLoaded && groupCallController.view.superview == nil { if groupCallController.isNodeLoaded && groupCallController.view.superview == nil {
groupCallController.sourcePanel = sourcePanel
mainWindow.hostView.containerView.endEditing(true) mainWindow.hostView.containerView.endEditing(true)
mainWindow.present(groupCallController, on: .calls) mainWindow.present(groupCallController, on: .calls)
} }