mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Call UI
This commit is contained in:
parent
61efee0d51
commit
5da3442266
@ -142,6 +142,8 @@ public final class ViewController: UIViewController {
|
||||
self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0))
|
||||
self.callState.remoteVideo = nil
|
||||
self.callState.localVideo = nil
|
||||
self.callState.isMicrophoneMuted = false
|
||||
self.callState.isRemoteBatteryLow = false
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
callScreenView.backAction = { [weak self] in
|
||||
@ -151,6 +153,12 @@ public final class ViewController: UIViewController {
|
||||
self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
callScreenView.closeAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.callScreenView?.speakerAction?()
|
||||
}
|
||||
}
|
||||
|
||||
private func update(transition: Transition) {
|
||||
|
@ -136,7 +136,7 @@ public final class CallController: ViewController {
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
if self.sharedContext.immediateExperimentalUISettings.callUIV2 {
|
||||
self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||
self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||
} else {
|
||||
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
|
||||
}
|
||||
|
@ -29,8 +29,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
private let callScreen: PrivateCallScreen
|
||||
private var callScreenState: PrivateCallScreen.State?
|
||||
|
||||
private var shouldStayHiddenUntilConnection: Bool = false
|
||||
|
||||
private var callStartTimestamp: Double?
|
||||
|
||||
private var callState: PresentationCallState?
|
||||
@ -67,7 +65,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
presentationData: PresentationData,
|
||||
statusBar: StatusBar,
|
||||
debugInfo: Signal<(String, String), NoError>,
|
||||
shouldStayHiddenUntilConnection: Bool = false,
|
||||
easyDebugAccess: Bool,
|
||||
call: PresentationCall
|
||||
) {
|
||||
@ -80,8 +77,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
self.containerView = UIView()
|
||||
self.callScreen = PrivateCallScreen()
|
||||
|
||||
self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.containerView)
|
||||
@ -123,6 +118,12 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
}
|
||||
self.back?()
|
||||
}
|
||||
self.callScreen.closeAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.dismissedInteractively?()
|
||||
}
|
||||
|
||||
self.callScreenState = PrivateCallScreen.State(
|
||||
lifecycleState: .connecting,
|
||||
@ -130,7 +131,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
shortName: " ",
|
||||
avatarImage: nil,
|
||||
audioOutput: .internalSpeaker,
|
||||
isMicrophoneMuted: false,
|
||||
isLocalAudioMuted: false,
|
||||
isRemoteAudioMuted: false,
|
||||
localVideo: nil,
|
||||
remoteVideo: nil,
|
||||
isRemoteBatteryLow: false
|
||||
@ -145,8 +147,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
return
|
||||
}
|
||||
self.isMuted = isMuted
|
||||
if callScreenState.isMicrophoneMuted != isMuted {
|
||||
callScreenState.isMicrophoneMuted = isMuted
|
||||
if callScreenState.isLocalAudioMuted != isMuted {
|
||||
callScreenState.isLocalAudioMuted = isMuted
|
||||
self.callScreenState = callScreenState
|
||||
self.update(transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
@ -373,6 +375,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
callScreenState.isRemoteBatteryLow = false
|
||||
}
|
||||
|
||||
switch callState.remoteAudioState {
|
||||
case .muted:
|
||||
callScreenState.isRemoteAudioMuted = true
|
||||
case .active:
|
||||
callScreenState.isRemoteAudioMuted = false
|
||||
}
|
||||
|
||||
if self.callScreenState != callScreenState {
|
||||
self.callScreenState = callScreenState
|
||||
self.update(transition: .animated(duration: 0.35, curve: .spring))
|
||||
@ -393,6 +402,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
return
|
||||
}
|
||||
callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
|
||||
callScreenState.shortName = peer.compactDisplayTitle
|
||||
|
||||
if self.currentPeer?.smallProfileImage != peer.smallProfileImage {
|
||||
self.peerAvatarDisposable?.dispose()
|
||||
@ -460,16 +470,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
self.containerView.layer.removeAnimation(forKey: "scale")
|
||||
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
if !self.shouldStayHiddenUntilConnection {
|
||||
self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
|
||||
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
|
||||
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
if !self.shouldStayHiddenUntilConnection || self.containerView.alpha > 0.0 {
|
||||
if self.containerView.alpha > 0.0 {
|
||||
self.containerView.layer.allowsGroupOpacity = true
|
||||
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
self?.containerView.layer.allowsGroupOpacity = false
|
||||
@ -499,7 +507,14 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
transition.updateFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
if let callScreenState = self.callScreenState {
|
||||
if var callScreenState = self.callScreenState {
|
||||
if case .terminated = callScreenState.lifecycleState {
|
||||
callScreenState.isLocalAudioMuted = false
|
||||
callScreenState.isRemoteAudioMuted = false
|
||||
callScreenState.isRemoteBatteryLow = false
|
||||
callScreenState.localVideo = nil
|
||||
callScreenState.remoteVideo = nil
|
||||
}
|
||||
self.callScreen.update(
|
||||
size: layout.size,
|
||||
insets: layout.insets(options: [.statusBar]),
|
||||
|
@ -67,6 +67,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -58,8 +58,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
|
||||
private var buttons: [Button]?
|
||||
private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:]
|
||||
|
||||
private var noticeViews: [AnyHashable: NoticeView] = [:]
|
||||
private var closeButtonView: CloseButtonView?
|
||||
|
||||
var closePressed: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -79,7 +81,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
return result
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
|
||||
func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
|
||||
self.buttons = buttons
|
||||
|
||||
let buttonSize: CGFloat = 56.0
|
||||
@ -163,6 +165,46 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
}
|
||||
var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5)
|
||||
|
||||
if displayClose {
|
||||
let closeButtonView: CloseButtonView
|
||||
var closeButtonTransition = transition
|
||||
var animateIn = false
|
||||
if let current = self.closeButtonView {
|
||||
closeButtonView = current
|
||||
} else {
|
||||
closeButtonTransition = closeButtonTransition.withAnimation(.none)
|
||||
animateIn = true
|
||||
|
||||
closeButtonView = CloseButtonView()
|
||||
self.closeButtonView = closeButtonView
|
||||
self.addSubview(closeButtonView)
|
||||
closeButtonView.pressAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.closePressed?()
|
||||
}
|
||||
}
|
||||
let closeButtonSize = CGSize(width: minWidth, height: buttonSize)
|
||||
closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition)
|
||||
closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize))
|
||||
|
||||
if animateIn && !transition.animation.isImmediate {
|
||||
closeButtonView.animateIn()
|
||||
}
|
||||
} else {
|
||||
if let closeButtonView = self.closeButtonView {
|
||||
self.closeButtonView = nil
|
||||
if !transition.animation.isImmediate {
|
||||
closeButtonView.animateOut(completion: { [weak closeButtonView] in
|
||||
closeButtonView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
closeButtonView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for button in buttons {
|
||||
let title: String
|
||||
let image: UIImage?
|
||||
@ -213,9 +255,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
Transition.immediate.setScale(view: buttonView, scale: 0.001)
|
||||
buttonView.alpha = 0.0
|
||||
transition.setScale(view: buttonView, scale: 1.0)
|
||||
transition.setAlpha(view: buttonView, alpha: 1.0)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0)
|
||||
|
||||
buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize)))
|
||||
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition)
|
||||
buttonX += buttonSize + buttonSpacing
|
||||
|
@ -0,0 +1,185 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import UIKitRuntimeUtils
|
||||
|
||||
final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol {
|
||||
private struct Params: Equatable {
|
||||
var text: String
|
||||
var size: CGSize
|
||||
|
||||
init(text: String, size: CGSize) {
|
||||
self.text = text
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
private let backdropBackgroundView: RoundedCornersView
|
||||
private let backgroundView: RoundedCornersView
|
||||
private let backgroundMaskView: UIView
|
||||
private let backgroundClippingView: UIView
|
||||
|
||||
private let duration: Double = 5.0
|
||||
private var fillTime: Double = 0.0
|
||||
|
||||
private let backgroundTextView: TextView
|
||||
private let backgroundTextClippingView: UIView
|
||||
|
||||
private let textView: TextView
|
||||
|
||||
var pressAction: (() -> Void)?
|
||||
|
||||
private var params: Params?
|
||||
private var updateDisplayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
let maskContents: UIView
|
||||
override static var layerClass: AnyClass {
|
||||
return MirroringLayer.self
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true)
|
||||
self.backdropBackgroundView.update(cornerRadius: 12.0, transition: .immediate)
|
||||
|
||||
self.backgroundView = RoundedCornersView(color: .white, smoothCorners: true)
|
||||
self.backgroundView.update(cornerRadius: 12.0, transition: .immediate)
|
||||
self.backgroundView.isUserInteractionEnabled = false
|
||||
|
||||
self.backgroundMaskView = UIView()
|
||||
self.backgroundMaskView.backgroundColor = .white
|
||||
self.backgroundView.mask = self.backgroundMaskView
|
||||
if let filter = makeLuminanceToAlphaFilter() {
|
||||
self.backgroundMaskView.layer.filters = [filter]
|
||||
}
|
||||
|
||||
self.backgroundClippingView = UIView()
|
||||
self.backgroundClippingView.clipsToBounds = true
|
||||
self.backgroundClippingView.layer.cornerRadius = 12.0
|
||||
|
||||
self.backgroundTextClippingView = UIView()
|
||||
self.backgroundTextClippingView.clipsToBounds = true
|
||||
|
||||
self.backgroundTextView = TextView()
|
||||
self.textView = TextView()
|
||||
|
||||
self.maskContents = UIView()
|
||||
|
||||
self.maskContents.addSubview(self.backdropBackgroundView)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
(self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer
|
||||
|
||||
self.backgroundTextClippingView.addSubview(self.backgroundTextView)
|
||||
self.backgroundTextClippingView.isUserInteractionEnabled = false
|
||||
self.addSubview(self.backgroundTextClippingView)
|
||||
|
||||
self.backgroundClippingView.addSubview(self.backgroundView)
|
||||
self.backgroundClippingView.isUserInteractionEnabled = false
|
||||
self.addSubview(self.backgroundClippingView)
|
||||
|
||||
self.backgroundMaskView.addSubview(self.textView)
|
||||
|
||||
self.internalHighligthedChanged = { [weak self] highlighted in
|
||||
if let self, self.bounds.width > 0.0 {
|
||||
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
|
||||
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
|
||||
|
||||
if highlighted {
|
||||
self.layer.removeAnimation(forKey: "sublayerTransform")
|
||||
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
||||
transition.setScale(layer: self.layer, scale: topScale)
|
||||
} else {
|
||||
let t = self.layer.presentation()?.transform ?? layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
|
||||
let transition = Transition(animation: .none)
|
||||
transition.setScale(layer: self.layer, scale: 1.0)
|
||||
|
||||
self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in
|
||||
guard let self, completed else {
|
||||
return
|
||||
}
|
||||
|
||||
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
(self.layer as? MirroringLayer)?.didEnterHierarchy = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.fillTime < self.duration && self.updateDisplayLink == nil {
|
||||
self.updateDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.fillTime = min(self.duration, self.fillTime + deltaTime)
|
||||
if let params = self.params {
|
||||
self.update(params: params, transition: .immediate)
|
||||
}
|
||||
|
||||
if self.fillTime >= self.duration {
|
||||
self.updateDisplayLink = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.pressAction?()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func update(text: String, size: CGSize, transition: Transition) {
|
||||
let params = Params(text: text, size: size)
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
self.params = params
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
private func update(params: Params, transition: Transition) {
|
||||
let fillFraction: CGFloat = CGFloat(self.fillTime / self.duration)
|
||||
|
||||
let sideInset: CGFloat = 12.0
|
||||
let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .black, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate)
|
||||
let _ = self.backgroundTextView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .white, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate)
|
||||
|
||||
transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
|
||||
let progressWidth: CGFloat = max(0.0, min(params.size.width, floorToScreenPixels(fillFraction * params.size.width)))
|
||||
let backgroundClippingFrame = CGRect(origin: CGPoint(x: progressWidth, y: 0.0), size: CGSize(width: params.size.width - progressWidth, height: params.size.height))
|
||||
transition.setPosition(view: self.backgroundClippingView, position: backgroundClippingFrame.center)
|
||||
transition.setBounds(view: self.backgroundClippingView, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX, y: 0.0), size: backgroundClippingFrame.size))
|
||||
|
||||
let backgroundTextClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: params.size.height))
|
||||
transition.setPosition(view: self.backgroundTextClippingView, position: backgroundTextClippingFrame.center)
|
||||
transition.setBounds(view: self.backgroundTextClippingView, bounds: CGRect(origin: CGPoint(), size: backgroundTextClippingFrame.size))
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((params.size.width - textSize.width) * 0.5), y: floor((params.size.height - textSize.height) * 0.5)), size: textSize)
|
||||
transition.setFrame(view: self.textView, frame: textFrame)
|
||||
transition.setFrame(view: self.backgroundTextView, frame: textFrame)
|
||||
}
|
||||
}
|
@ -63,8 +63,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
||||
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
|
||||
|
||||
if highlighted {
|
||||
self.layer.removeAnimation(forKey: "opacity")
|
||||
self.layer.removeAnimation(forKey: "transform")
|
||||
self.layer.removeAnimation(forKey: "sublayerTransform")
|
||||
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
||||
transition.setScale(layer: self.layer, scale: topScale)
|
||||
} else {
|
||||
|
@ -5,10 +5,10 @@ import ComponentFlow
|
||||
|
||||
final class EmojiExpandedInfoView: OverlayMaskContainerView {
|
||||
private struct Params: Equatable {
|
||||
var constrainedWidth: CGFloat
|
||||
var width: CGFloat
|
||||
|
||||
init(constrainedWidth: CGFloat) {
|
||||
self.constrainedWidth = constrainedWidth
|
||||
init(width: CGFloat) {
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,8 +129,8 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView {
|
||||
return nil
|
||||
}
|
||||
|
||||
func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize {
|
||||
let params = Params(constrainedWidth: constrainedWidth)
|
||||
func update(width: CGFloat, transition: Transition) -> CGSize {
|
||||
let params = Params(width: width)
|
||||
if let currentLayout = self.currentLayout, currentLayout.params == params {
|
||||
return currentLayout.size
|
||||
}
|
||||
@ -142,16 +142,12 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView {
|
||||
private func update(params: Params, transition: Transition) -> CGSize {
|
||||
let buttonHeight: CGFloat = 56.0
|
||||
|
||||
var constrainedWidth = params.constrainedWidth
|
||||
constrainedWidth = min(constrainedWidth, 300.0)
|
||||
let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition)
|
||||
let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition)
|
||||
|
||||
let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition)
|
||||
let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition)
|
||||
|
||||
let contentWidth: CGFloat = max(titleSize.width, textSize.width) + 26.0 * 2.0
|
||||
let contentHeight = 78.0 + titleSize.height + 10.0 + textSize.height + 22.0 + buttonHeight
|
||||
|
||||
let size = CGSize(width: contentWidth, height: contentHeight)
|
||||
let size = CGSize(width: params.width, height: contentHeight)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
|
@ -198,8 +198,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
|
||||
encoder.setFragmentTexture(blurredTexture, index: 0)
|
||||
|
||||
var brightness: Float = 1.0
|
||||
var saturation: Float = 1.2
|
||||
var brightness: Float = 0.7
|
||||
var saturation: Float = 1.3
|
||||
var overlay: SIMD4<Float> = SIMD4<Float>(1.0, 1.0, 1.0, 0.2)
|
||||
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
|
||||
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
||||
|
@ -0,0 +1,73 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
final class RatingView: OverlayMaskContainerView {
|
||||
private let backgroundView: RoundedCornersView
|
||||
private let textContainer: UIView
|
||||
private let textView: TextView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = RoundedCornersView(color: .white)
|
||||
self.textContainer = UIView()
|
||||
self.textContainer.clipsToBounds = true
|
||||
self.textView = TextView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.maskContents.addSubview(self.backgroundView)
|
||||
|
||||
self.textContainer.addSubview(self.textView)
|
||||
self.addSubview(self.textContainer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let delay: Double = 0.2
|
||||
|
||||
self.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
self.textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay)
|
||||
|
||||
self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
self.backgroundView.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.backgroundView.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.textContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
||||
self.textContainer.layer.cornerRadius = self.bounds.height * 0.5
|
||||
self.textContainer.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.textContainer.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in
|
||||
guard let self, completed else {
|
||||
return
|
||||
}
|
||||
self.textContainer.layer.cornerRadius = 0.0
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func update(text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize {
|
||||
let sideInset: CGFloat = 12.0
|
||||
let verticalInset: CGFloat = 6.0
|
||||
|
||||
let textSize = self.textView.update(string: text, fontSize: 15.0, fontWeight: 0.0, color: .white, constrainedWidth: constrainedWidth - sideInset * 2.0, transition: .immediate)
|
||||
let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundView.update(cornerRadius: floor(size.height * 0.5), transition: transition)
|
||||
|
||||
transition.setFrame(view: self.textContainer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setFrame(view: self.textView, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize))
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
@ -5,17 +5,16 @@ import ComponentFlow
|
||||
|
||||
final class RoundedCornersView: UIImageView {
|
||||
private let color: UIColor
|
||||
private let smoothCorners: Bool
|
||||
|
||||
private var currentCornerRadius: CGFloat?
|
||||
private var cornerImage: UIImage?
|
||||
|
||||
init(color: UIColor) {
|
||||
init(color: UIColor, smoothCorners: Bool = false) {
|
||||
self.color = color
|
||||
self.smoothCorners = smoothCorners
|
||||
|
||||
super.init(image: nil)
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
self.layer.cornerCurve = .circular
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -26,10 +25,23 @@ final class RoundedCornersView: UIImageView {
|
||||
guard let cornerRadius = self.currentCornerRadius else {
|
||||
return
|
||||
}
|
||||
if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 {
|
||||
if self.smoothCorners {
|
||||
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
|
||||
if let cornerImage = self.cornerImage, cornerImage.size == size {
|
||||
} else {
|
||||
self.cornerImage = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
|
||||
context.setFillColor(self.color.cgColor)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5)
|
||||
}
|
||||
} else {
|
||||
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
|
||||
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
|
||||
if let cornerImage = self.cornerImage, cornerImage.size == size {
|
||||
} else {
|
||||
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
|
||||
}
|
||||
}
|
||||
self.image = self.cornerImage
|
||||
self.clipsToBounds = false
|
||||
@ -52,6 +64,14 @@ final class RoundedCornersView: UIImageView {
|
||||
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
|
||||
self.layer.cornerRadius = previousCornerRadius
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
if self.smoothCorners {
|
||||
self.layer.cornerCurve = .continuous
|
||||
} else {
|
||||
self.layer.cornerCurve = .circular
|
||||
}
|
||||
|
||||
}
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in
|
||||
guard let self, completed else {
|
||||
return
|
||||
|
@ -100,7 +100,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
public var shortName: String
|
||||
public var avatarImage: UIImage?
|
||||
public var audioOutput: AudioOutput
|
||||
public var isMicrophoneMuted: Bool
|
||||
public var isLocalAudioMuted: Bool
|
||||
public var isRemoteAudioMuted: Bool
|
||||
public var localVideo: VideoSource?
|
||||
public var remoteVideo: VideoSource?
|
||||
public var isRemoteBatteryLow: Bool
|
||||
@ -111,7 +112,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
shortName: String,
|
||||
avatarImage: UIImage?,
|
||||
audioOutput: AudioOutput,
|
||||
isMicrophoneMuted: Bool,
|
||||
isLocalAudioMuted: Bool,
|
||||
isRemoteAudioMuted: Bool,
|
||||
localVideo: VideoSource?,
|
||||
remoteVideo: VideoSource?,
|
||||
isRemoteBatteryLow: Bool
|
||||
@ -121,7 +123,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self.shortName = shortName
|
||||
self.avatarImage = avatarImage
|
||||
self.audioOutput = audioOutput
|
||||
self.isMicrophoneMuted = isMicrophoneMuted
|
||||
self.isLocalAudioMuted = isLocalAudioMuted
|
||||
self.isRemoteAudioMuted = isRemoteAudioMuted
|
||||
self.localVideo = localVideo
|
||||
self.remoteVideo = remoteVideo
|
||||
self.isRemoteBatteryLow = isRemoteBatteryLow
|
||||
@ -143,7 +146,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
if lhs.audioOutput != rhs.audioOutput {
|
||||
return false
|
||||
}
|
||||
if lhs.isMicrophoneMuted != rhs.isMicrophoneMuted {
|
||||
if lhs.isLocalAudioMuted != rhs.isLocalAudioMuted {
|
||||
return false
|
||||
}
|
||||
if lhs.isRemoteAudioMuted != rhs.isRemoteAudioMuted {
|
||||
return false
|
||||
}
|
||||
if lhs.localVideo !== rhs.localVideo {
|
||||
@ -224,6 +230,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
public var microhoneMuteAction: (() -> Void)?
|
||||
public var endCallAction: (() -> Void)?
|
||||
public var backAction: (() -> Void)?
|
||||
public var closeAction: (() -> Void)?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.overlayContentsView = UIView()
|
||||
@ -264,10 +271,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self.avatarTransformLayer.addSublayer(self.avatarLayer)
|
||||
self.layer.addSublayer(self.avatarTransformLayer)
|
||||
|
||||
/*let edgeTestLayer = EdgeTestLayer()
|
||||
edgeTestLayer.frame = CGRect(origin: CGPoint(x: 20.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0))
|
||||
self.layer.addSublayer(edgeTestLayer)*/
|
||||
|
||||
self.addSubview(self.videoContainerBackgroundView)
|
||||
|
||||
self.overlayContentsView.mask = self.maskContents
|
||||
@ -310,6 +313,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
self.backAction?()
|
||||
}
|
||||
|
||||
self.buttonGroupView.closePressed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.closeAction?()
|
||||
}
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
@ -497,6 +507,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||
|
||||
let wideContentWidth: CGFloat
|
||||
if params.size.width < 500.0 {
|
||||
wideContentWidth = params.size.width - 44.0 * 2.0
|
||||
} else {
|
||||
wideContentWidth = 400.0
|
||||
}
|
||||
|
||||
var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = []
|
||||
if self.swapLocalAndRemoteVideo {
|
||||
if let activeLocalVideoSource = self.activeLocalVideoSource {
|
||||
@ -554,7 +571,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
self.videoAction?()
|
||||
}),
|
||||
ButtonGroupView.Button(content: .microphone(isMuted: params.state.isMicrophoneMuted), action: { [weak self] in
|
||||
ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -584,9 +601,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
var notices: [ButtonGroupView.Notice] = []
|
||||
if params.state.isMicrophoneMuted {
|
||||
if params.state.isLocalAudioMuted {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off"))
|
||||
}
|
||||
if params.state.isRemoteAudioMuted {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "\(params.state.shortName)'s microphone is turned off"))
|
||||
}
|
||||
if params.state.remoteVideo != nil && params.state.localVideo == nil {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "Your camera is turned off"))
|
||||
}
|
||||
@ -594,7 +614,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "\(params.state.shortName)'s battery is low"))
|
||||
}
|
||||
|
||||
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, notices: notices, transition: transition)
|
||||
var displayClose = false
|
||||
if case .terminated = params.state.lifecycleState {
|
||||
displayClose = true
|
||||
}
|
||||
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition)
|
||||
|
||||
var expandedEmojiKeyRect: CGRect?
|
||||
if self.isEmojiKeyExpanded {
|
||||
@ -632,7 +656,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
}
|
||||
|
||||
let emojiExpandedInfoSize = emojiExpandedInfoView.update(constrainedWidth: params.size.width - (params.insets.left + 16.0) * 2.0, transition: emojiExpandedInfoTransition)
|
||||
let emojiExpandedInfoSize = emojiExpandedInfoView.update(width: wideContentWidth, transition: emojiExpandedInfoTransition)
|
||||
let emojiExpandedInfoFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiExpandedInfoSize.width) * 0.5), y: params.insets.top + 73.0), size: emojiExpandedInfoSize)
|
||||
emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height))
|
||||
emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size))
|
||||
|
@ -132,8 +132,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
private var groupCallDisposable: Disposable?
|
||||
|
||||
private var callController: CallController?
|
||||
private var call: PresentationCall?
|
||||
public let hasOngoingCall = ValuePromise<Bool>(false)
|
||||
private let callState = Promise<PresentationCallState?>(nil)
|
||||
private var awaitingCallConnectionDisposable: Disposable?
|
||||
|
||||
private var groupCallController: VoiceChatController?
|
||||
public var currentGroupCallController: ViewController? {
|
||||
@ -741,26 +743,49 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
|
||||
self.callDisposable = (callManager.currentCallSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] call in
|
||||
if let strongSelf = self {
|
||||
if call !== strongSelf.callController?.call {
|
||||
strongSelf.callController?.dismiss()
|
||||
strongSelf.callController = nil
|
||||
strongSelf.hasOngoingCall.set(false)
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if call !== self.call {
|
||||
self.call = call
|
||||
|
||||
self.callController?.dismiss()
|
||||
self.callController = nil
|
||||
self.hasOngoingCall.set(false)
|
||||
|
||||
if let call {
|
||||
self.callState.set(call.state
|
||||
|> map(Optional.init))
|
||||
self.hasOngoingCall.set(true)
|
||||
setNotificationCall(call)
|
||||
|
||||
if let call = call {
|
||||
mainWindow.hostView.containerView.endEditing(true)
|
||||
let callController = CallController(sharedContext: strongSelf, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild)
|
||||
strongSelf.callController = callController
|
||||
strongSelf.mainWindow?.present(callController, on: .calls)
|
||||
strongSelf.callState.set(call.state
|
||||
|> map(Optional.init))
|
||||
strongSelf.hasOngoingCall.set(true)
|
||||
setNotificationCall(call)
|
||||
} else {
|
||||
strongSelf.callState.set(.single(nil))
|
||||
strongSelf.hasOngoingCall.set(false)
|
||||
setNotificationCall(nil)
|
||||
if !call.isOutgoing && call.isIntegratedWithCallKit {
|
||||
self.awaitingCallConnectionDisposable = (call.state
|
||||
|> filter { state in
|
||||
switch state.state {
|
||||
case .ringing:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentControllerWithCurrentCall()
|
||||
})
|
||||
} else{
|
||||
self.presentControllerWithCurrentCall()
|
||||
}
|
||||
} else {
|
||||
self.callState.set(.single(nil))
|
||||
self.hasOngoingCall.set(false)
|
||||
self.awaitingCallConnectionDisposable?.dispose()
|
||||
self.awaitingCallConnectionDisposable = nil
|
||||
setNotificationCall(nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -951,6 +976,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
self.callDisposable?.dispose()
|
||||
self.groupCallDisposable?.dispose()
|
||||
self.callStateDisposable?.dispose()
|
||||
self.awaitingCallConnectionDisposable?.dispose()
|
||||
}
|
||||
|
||||
private var didPerformAccountSettingsImport = false
|
||||
@ -1010,6 +1036,27 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentControllerWithCurrentCall() {
|
||||
guard let call = self.call else {
|
||||
return
|
||||
}
|
||||
|
||||
if let currentCallController = self.callController {
|
||||
if currentCallController.call === call {
|
||||
self.navigateToCurrentCall()
|
||||
return
|
||||
} else {
|
||||
self.callController = nil
|
||||
currentCallController.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
self.mainWindow?.hostView.containerView.endEditing(true)
|
||||
let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild)
|
||||
self.callController = callController
|
||||
self.mainWindow?.present(callController, on: .calls)
|
||||
}
|
||||
|
||||
public func updateNotificationTokensRegistration() {
|
||||
let sandbox: Bool
|
||||
#if DEBUG
|
||||
|
Loading…
x
Reference in New Issue
Block a user