diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7b396c5bc7..58bce9ff44 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/Tests/CallUITest/BUILD b/Tests/CallUITest/BUILD index a809c0ad08..7cb591d917 100644 --- a/Tests/CallUITest/BUILD +++ b/Tests/CallUITest/BUILD @@ -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" - ) diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index 7641bd93e7..3178bdc66b 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -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", diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 812b95aee7..5fa6b1cd5a 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -157,6 +157,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } self.callScreenState = PrivateCallScreen.State( + strings: presentationData.strings, lifecycleState: .connecting, name: " ", shortName: " ", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index ac35b7c0f2..6773bd07e8 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -68,6 +68,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/AppBundle", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json new file mode 100644 index 0000000000..de2d610148 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json @@ -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 + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png new file mode 100644 index 0000000000..2dddf3fa21 Binary files /dev/null and b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png differ diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift index 0fed60cf04..696d0a5727 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift @@ -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 + } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 180fa18dca..73c36b48ce 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index a53cfd48ad..b974b71dc7 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -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) } }