Add call localization

This commit is contained in:
Isaac 2023-12-30 00:56:25 +04:00
parent d88c78da57
commit 3fb85da558
10 changed files with 197 additions and 41 deletions

View File

@ -4418,6 +4418,7 @@ Sorry for the inconvenience.";
"Call.Mute" = "mute";
"Call.Camera" = "camera";
"Call.Video" = "video";
"Call.Flip" = "flip";
"Call.End" = "end";
"Call.Speaker" = "speaker";
@ -5646,6 +5647,7 @@ Sorry for the inconvenience.";
"Call.CameraConfirmationText" = "Switch to video call?";
"Call.CameraConfirmationConfirm" = "Switch";
"Call.YourCameraOff" = "Your camera is off";
"Call.YourMicrophoneOff" = "Your microphone is off";
"Call.MicrophoneOff" = "%@'s microphone is off";
"Call.CameraOff" = "%@'s camera is off";
@ -10867,3 +10869,12 @@ Sorry for the inconvenience.";
"Chat.PlayOnceMesasgeClose" = "Close";
"Chat.PlayOnceMesasgeCloseAndDelete" = "Close and Delete";
"Chat.PlayOnceMesasge.DisableScreenCapture" = "Sorry, you can't play this message while screen recording is active.";
"Call.EncryptedAlertTitle" = "This call is end-to-end encrypted";
"Call.EncryptedAlertText" = "If the emoji on %@'s screen are the same, this call is 100% secure.";
"Call.EncryptionKeyTooltip" = "Encryption key of this call";
"Call.StatusBusy" = "Line Busy";
"Call.StatusDeclined" = "Call Declined";
"Call.StatusFailed" = "Call Failed";
"Call.StatusEnded" = "Call Ended";
"Call.StatusMissed" = "Call Missed";

View File

@ -43,6 +43,7 @@ swift_library(
deps = [
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/Calls/CallScreen",
],
)
@ -188,5 +189,4 @@ xcodeproj(
},
},
default_xcode_configuration = "Debug"
)

View File

@ -4,6 +4,7 @@ import MetalEngine
import Display
import CallScreen
import ComponentFlow
import TelegramPresentationData
private extension UIScreen {
private static let cornerRadiusKey: String = {
@ -24,6 +25,7 @@ private extension UIScreen {
public final class ViewController: UIViewController {
private var callScreenView: PrivateCallScreen?
private var callState: PrivateCallScreen.State = PrivateCallScreen.State(
strings: defaultPresentationStrings,
lifecycleState: .connecting,
name: "Emma Walters",
shortName: "Emma",

View File

@ -157,6 +157,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
self.callScreenState = PrivateCallScreen.State(
strings: presentationData.strings,
lifecycleState: .connecting,
name: " ",
shortName: " ",

View File

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

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Snow.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -3,34 +3,39 @@ import UIKit
import Display
final class BackButtonView: HighlightableButton {
private struct Params: Equatable {
var text: String
init(text: String) {
self.text = text
}
}
private struct Layout: Equatable {
var params: Params
var size: CGSize
init(params: Params, size: CGSize) {
self.params = params
self.size = size
}
}
private let iconView: UIImageView
private let textView: TextView
let size: CGSize
private var currentLayout: Layout?
var pressAction: (() -> Void)?
init(text: String) {
override init(frame: CGRect) {
self.iconView = UIImageView(image: NavigationBar.backArrowImage(color: .white))
self.iconView.isUserInteractionEnabled = false
self.textView = TextView()
self.textView.isUserInteractionEnabled = false
let spacing: CGFloat = 8.0
var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0)
let iconScaleFactor: CGFloat = 0.9
iconSize.width = floor(iconSize.width * iconScaleFactor)
iconSize.height = floor(iconSize.height * iconScaleFactor)
let textSize = self.textView.update(string: text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate)
self.size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height)
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.size.height - iconSize.height) * 0.5)), size: iconSize)
self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((self.size.height - textSize.height) * 0.5)), size: textSize)
super.init(frame: CGRect())
super.init(frame: frame)
self.addSubview(self.iconView)
self.addSubview(self.textView)
@ -53,4 +58,31 @@ final class BackButtonView: HighlightableButton {
return nil
}
}
func update(text: String) -> CGSize {
let params = Params(text: text)
if let currentLayout = self.currentLayout, currentLayout.params == params {
return currentLayout.size
}
let size = self.update(params: params)
self.currentLayout = Layout(params: params, size: size)
return size
}
private func update(params: Params) -> CGSize {
let spacing: CGFloat = 8.0
var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0)
let iconScaleFactor: CGFloat = 0.9
iconSize.width = floor(iconSize.width * iconScaleFactor)
iconSize.height = floor(iconSize.height * iconScaleFactor)
let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate)
let size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height)
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - iconSize.height) * 0.5)), size: iconSize)
self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize)
return size
}
}

View File

@ -3,6 +3,7 @@ import UIKit
import Display
import ComponentFlow
import AppBundle
import TelegramPresentationData
final class ButtonGroupView: OverlayMaskContainerView {
final class Button {
@ -85,7 +86,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
return result
}
func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
self.buttons = buttons
let buttonSize: CGFloat = 56.0
@ -190,7 +191,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
}
}
let closeButtonSize = CGSize(width: minWidth, height: buttonSize)
closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition)
closeButtonView.update(text: strings.Common_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 {
@ -218,9 +219,9 @@ final class ButtonGroupView: OverlayMaskContainerView {
case let .speaker(audioOutput):
switch audioOutput {
case .internalSpeaker, .speaker:
title = "speaker"
title = strings.Call_Speaker
default:
title = "audio"
title = strings.Call_Audio
}
switch audioOutput {
@ -247,19 +248,19 @@ final class ButtonGroupView: OverlayMaskContainerView {
isActive = true
}
case .flipCamera:
title = "flip"
title = strings.Call_Flip
image = UIImage(bundleImageName: "Call/Flip")
isActive = false
case let .video(isActiveValue):
title = "video"
title = strings.Call_Video
image = UIImage(bundleImageName: "Call/Video")
isActive = isActiveValue
case let .microphone(isActiveValue):
title = "mute"
title = strings.Call_Mute
image = UIImage(bundleImageName: "Call/Mute")
isActive = isActiveValue
case .end:
title = "end"
title = strings.Call_End
image = UIImage(bundleImageName: "Call/End")
isActive = false
isDestructive = true

View File

@ -7,6 +7,7 @@ import MetalEngine
import ComponentFlow
import SwiftSignalKit
import UIKitRuntimeUtils
import TelegramPresentationData
public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate {
public struct State: Equatable {
@ -67,6 +68,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
case bluetooth
}
public var strings: PresentationStrings
public var lifecycleState: LifecycleState
public var name: String
public var shortName: String
@ -77,8 +79,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
public var localVideo: VideoSource?
public var remoteVideo: VideoSource?
public var isRemoteBatteryLow: Bool
public var displaySnowEffect: Bool
public init(
strings: PresentationStrings,
lifecycleState: LifecycleState,
name: String,
shortName: String,
@ -88,8 +92,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
isRemoteAudioMuted: Bool,
localVideo: VideoSource?,
remoteVideo: VideoSource?,
isRemoteBatteryLow: Bool
isRemoteBatteryLow: Bool,
displaySnowEffect: Bool = false
) {
self.strings = strings
self.lifecycleState = lifecycleState
self.name = name
self.shortName = shortName
@ -100,9 +106,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.localVideo = localVideo
self.remoteVideo = remoteVideo
self.isRemoteBatteryLow = isRemoteBatteryLow
self.displaySnowEffect = displaySnowEffect
}
public static func ==(lhs: State, rhs: State) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.lifecycleState != rhs.lifecycleState {
return false
}
@ -133,6 +143,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
if lhs.isRemoteBatteryLow != rhs.isRemoteBatteryLow {
return false
}
if lhs.displaySnowEffect != rhs.displaySnowEffect {
return false
}
return true
}
}
@ -218,6 +231,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
private var pipVideoCallViewController: UIViewController?
private var pipController: AVPictureInPictureController?
private var snowEffectView: SnowEffectView?
public override init(frame: CGRect) {
self.overlayContentsView = UIView()
self.overlayContentsView.isUserInteractionEnabled = false
@ -240,7 +255,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.titleView = TextView()
self.statusView = StatusView()
self.backButtonView = BackButtonView(text: "Back")
self.backButtonView = BackButtonView(frame: CGRect())
self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize()))
@ -740,16 +755,16 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
var notices: [ButtonGroupView.Notice] = []
if !isTerminated {
if params.state.isLocalAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: "Your microphone is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: params.state.strings.Call_YourMicrophoneOff))
}
if params.state.isRemoteAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: "\(params.state.shortName)'s microphone is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: params.state.strings.Call_MicrophoneOff(params.state.shortName).string))
}
if params.state.remoteVideo != nil && params.state.localVideo == nil {
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: "Your camera is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: params.state.strings.Call_YourCameraOff))
}
if params.state.isRemoteBatteryLow {
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: "\(params.state.shortName)'s battery is low"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: params.state.strings.Call_BatteryLow(params.state.shortName).string))
}
}
@ -759,7 +774,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
}*/
let displayClose = false
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition)
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, transition: transition)
var expandedEmojiKeyRect: CGRect?
if self.isEmojiKeyExpanded {
@ -777,7 +792,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
alphaTransition = genericAlphaTransition
}
emojiExpandedInfoView = EmojiExpandedInfoView(title: "This call is end-to-end encrypted", text: "If the emoji on \(params.state.shortName)'s screen are the same, this call is 100% secure.")
emojiExpandedInfoView = EmojiExpandedInfoView(title: params.state.strings.Call_EncryptedAlertTitle, text: params.state.strings.Call_EncryptedAlertText(params.state.shortName).string)
self.emojiExpandedInfoView = emojiExpandedInfoView
emojiExpandedInfoView.alpha = 0.0
Transition.immediate.setScale(view: emojiExpandedInfoView, scale: 0.5)
@ -824,13 +839,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
}
}
let backButtonSize = self.backButtonView.update(text: params.state.strings.Common_Back)
let backButtonY: CGFloat
if currentAreControlsHidden {
backButtonY = -self.backButtonView.size.height - 12.0
backButtonY = -backButtonSize.height - 12.0
} else {
backButtonY = params.insets.top + 12.0
}
let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: self.backButtonView.size)
let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: backButtonSize)
transition.setFrame(view: self.backButtonView, frame: backButtonFrame)
transition.setAlpha(view: self.backButtonView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
@ -914,7 +931,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
emojiTooltipView = current
} else {
emojiTooltipTransition = emojiTooltipTransition.withAnimation(.none)
emojiTooltipView = EmojiTooltipView(text: "Encryption key of this call")
emojiTooltipView = EmojiTooltipView(text: params.state.strings.Call_EncryptionKeyTooltip)
animateIn = true
self.emojiTooltipView = emojiTooltipView
self.addSubview(emojiTooltipView)
@ -1192,15 +1209,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
switch terminatedState.reason {
case .busy:
titleString = "Line Busy"
titleString = params.state.strings.Call_StatusBusy
case .declined:
titleString = "Call Declined"
titleString = params.state.strings.Call_StatusDeclined
case .failed:
titleString = "Call Failed"
titleString = params.state.strings.Call_StatusFailed
case .hangUp:
titleString = "Call Ended"
titleString = params.state.strings.Call_StatusEnded
case .missed:
titleString = "Call Missed"
titleString = params.state.strings.Call_StatusMissed
}
default:
displayAudioLevelBlob = !params.state.isRemoteAudioMuted
@ -1358,5 +1375,75 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
})
}
}
/*if params.state.displaySnowEffect {
let snowEffectView: SnowEffectView
if let current = self.snowEffectView {
snowEffectView = current
} else {
snowEffectView = SnowEffectView(frame: CGRect())
self.snowEffectView = snowEffectView
self.maskContents.addSubview(snowEffectView)
}
transition.setFrame(view: snowEffectView, frame: CGRect(origin: CGPoint(), size: params.size))
snowEffectView.update(size: params.size)
} else {
if let snowEffectView = self.snowEffectView {
self.snowEffectView = nil
snowEffectView.removeFromSuperview()
}
}*/
}
}
final class SnowEffectView: UIView {
private let particlesLayer: CAEmitterLayer
override init(frame: CGRect) {
let particlesLayer = CAEmitterLayer()
self.particlesLayer = particlesLayer
self.particlesLayer.backgroundColor = nil
self.particlesLayer.isOpaque = false
particlesLayer.emitterShape = .circle
particlesLayer.emitterMode = .surface
particlesLayer.renderMode = .oldestLast
let image1 = UIImage(named: "Call/Snow")?.cgImage
let cell1 = CAEmitterCell()
cell1.contents = image1
cell1.name = "Snow"
cell1.birthRate = 92.0
cell1.lifetime = 20.0
cell1.velocity = 59.0
cell1.velocityRange = -15.0
cell1.xAcceleration = 5.0
cell1.yAcceleration = 40.0
cell1.emissionRange = 90.0 * (.pi / 180.0)
cell1.spin = -28.6 * (.pi / 180.0)
cell1.spinRange = 57.2 * (.pi / 180.0)
cell1.scale = 0.06
cell1.scaleRange = 0.3
cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
particlesLayer.emitterCells = [cell1]
super.init(frame: frame)
self.layer.addSublayer(particlesLayer)
self.clipsToBounds = true
self.backgroundColor = nil
self.isOpaque = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize) {
self.particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
self.particlesLayer.emitterSize = CGSize(width: size.width * 3.0, height: size.height * 2.0)
self.particlesLayer.emitterPosition = CGPoint(x: size.width * 0.5, y: -325.0)
}
}