[WIP] Call UI

This commit is contained in:
Isaac 2023-12-06 00:51:21 +04:00
parent 61efee0d51
commit 5da3442266
13 changed files with 482 additions and 71 deletions

View File

@ -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) {

View File

@ -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)
}

View File

@ -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]),

View File

@ -67,6 +67,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/AppBundle",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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))

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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))

View File

@ -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