Add ZRTP Popup

This commit is contained in:
QuentinArguillere 2024-01-23 13:17:57 +01:00
parent 99b4868f7e
commit 9ef28d00f6
9 changed files with 478 additions and 95 deletions

View file

@ -55,6 +55,7 @@
D74C9CFF2ACAEC5E0021626A /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CFE2ACAEC5E0021626A /* PopupView.swift */; };
D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9D002ACB098C0021626A /* PermissionManager.swift */; };
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */; };
D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75759312B56D40900E7AC10 /* ZRTPPopup.swift */; };
D76005F62B0798B00054B79A /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76005F52B0798B00054B79A /* IntExtension.swift */; };
D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7702EF12AC7205000557C00 /* WelcomeView.swift */; };
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; };
@ -148,6 +149,7 @@
D74C9CFE2ACAEC5E0021626A /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = "<group>"; };
D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = "<group>"; };
D75759312B56D40900E7AC10 /* ZRTPPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZRTPPopup.swift; sourceTree = "<group>"; };
D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = "<group>"; };
D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = "<group>"; };
@ -401,6 +403,14 @@
path = Fragments;
sourceTree = "<group>";
};
D75759302B56D3CE00E7AC10 /* Fragments */ = {
isa = PBXGroup;
children = (
D75759312B56D40900E7AC10 /* ZRTPPopup.swift */,
);
path = Fragments;
sourceTree = "<group>";
};
D7702EF02AC7200600557C00 /* Welcome */ = {
isa = PBXGroup;
children = (
@ -490,6 +500,7 @@
D7B5678C2B28883700DE63EB /* Call */ = {
isa = PBXGroup;
children = (
D75759302B56D3CE00E7AC10 /* Fragments */,
D7B99E972B29B37F00BE7BF2 /* ViewModel */,
D7B5678D2B28888F00DE63EB /* CallView.swift */,
);
@ -702,6 +713,7 @@
D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */,
D76005F62B0798B00054B79A /* IntExtension.swift in Sources */,
D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */,
D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */,
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */,
D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */,
D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */,

View file

@ -73,7 +73,7 @@ final class CoreContext: ObservableObject {
Factory.Instance.logCollectionPath = configDir
Factory.Instance.enableLogCollection(state: LogCollectionState.Enabled)
Log.info("Initialising core")
let url = NSURL(fileURLWithPath: configDir)
if let pathComponent = url.appendingPathComponent("linphonerc") {
@ -102,6 +102,9 @@ final class CoreContext: ObservableObject {
self.mCore.setUserAgent(name: "Linphone iOS 6.0 Beta (\(UIDevice.current.localizedModel)) - Linphone SDK : \(self.coreVersion)", version: "6.0")
self.mCore.videoCaptureEnabled = true
self.mCore.videoDisplayEnabled = true
self.mCoreSuscriptions.insert(self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in
if cbVal.state == GlobalState.On {
self.defaultAccount = self.mCore.defaultAccount
@ -138,9 +141,6 @@ final class CoreContext: ObservableObject {
}
})
self.mCore.videoCaptureEnabled = true
self.mCore.videoDisplayEnabled = true
// Create a Core listener to listen for the callback we need
// In this case, we want to know about the account registration status
self.mCoreSuscriptions.insert(self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in
@ -161,7 +161,7 @@ final class CoreContext: ObservableObject {
// If account has been configured correctly, we will go through Progress and Ok states
// Otherwise, we will be Failed.
Log.info("New registration state is \(cbVal.state) for user id " +
"\( String(describing: cbVal.account.params?.identityAddress?.asString())) = \(cbVal.message)\n")
"\( String(describing: cbVal.account.params?.identityAddress?.asString())) = \(cbVal.message)\n")
if cbVal.state == .Ok {
self.loggingInProgress = false
self.loggedIn = true

View file

@ -43,7 +43,7 @@
"**Micro** : Pour permettre à vos correspondants de vous entendre." : {
},
"**Notifications** : Pour vous informé quand vous recevez un message ou un appel." : {
"**Notifications** : Pour vous informer quand vous recevez un message ou un appel." : {
},
"#" : {
@ -358,6 +358,9 @@
},
"Last name" : {
},
"Letters don't match!" : {
},
"Linphone" : {
@ -474,6 +477,9 @@
},
"Remove picture" : {
},
"Say %@ and click on the letters given by your correspondent:" : {
},
"Scan QR code" : {
@ -528,6 +534,9 @@
},
"The user name or password is incorrects" : {
},
"This call is completely securised" : {
},
"This contact will be deleted definitively." : {
@ -581,6 +590,9 @@
},
"Username error" : {
},
"Validate the device" : {
},
"Video Call" : {

View file

@ -44,8 +44,8 @@ class TelecomManager: ObservableObject {
@Published var callStarted: Bool = false
@Published var outgoingCallStarted: Bool = false
@Published var remoteVideo: Bool = false
@Published var isRecordingByRemote: Bool = false
@Published var isPausedByRemote: Bool = false
@Published var isRecordingByRemote: Bool = false
@Published var isPausedByRemote: Bool = false
var actionToFulFill: CXCallAction?
var callkitAudioSessionActivated: Bool?
@ -130,7 +130,7 @@ class TelecomManager: ObservableObject {
}
}
private func makeRecordFilePath() -> String{
private func makeRecordFilePath() -> String {
var filePath = "recording_"
let now = Date()
let dateFormat = DateFormatter()
@ -408,11 +408,10 @@ class TelecomManager: ObservableObject {
case .IncomingReceived:
let addr = call.remoteAddress
let displayName = incomingDisplayName(call: call)
#if targetEnvironment(simulator)
DispatchQueue.main.async {
self.outgoingCallStarted = false
self.callStarted = true
self.callStarted = false
if self.callInProgress == false {
withAnimation {
self.callInProgress = true

View file

@ -49,23 +49,32 @@ struct CallView: View {
var body: some View {
GeometryReader { geo in
if #available(iOS 16.0, *), idiom != .pad {
innerView(geometry: geo)
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}) {
innerBottomSheet()
.presentationDetents([.fraction(0.3)])
}
} else {
innerView(geometry: geo)
.halfSheet(showSheet: $audioRouteSheet) {
innerBottomSheet()
} onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}
ZStack {
if #available(iOS 16.0, *), idiom != .pad {
innerView(geometry: geo)
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}) {
innerBottomSheet()
.presentationDetents([.fraction(0.3)])
}
} else {
innerView(geometry: geo)
.halfSheet(showSheet: $audioRouteSheet) {
innerBottomSheet()
} onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}
}
if callViewModel.zrtpPopupDisplayed == true {
ZRTPPopup(callViewModel: callViewModel)
.background(.black.opacity(0.65))
.onTapGesture {
callViewModel.zrtpPopupDisplayed = false
}
}
}
}
}
@ -243,6 +252,17 @@ struct CallView: View {
Spacer()
if callViewModel.isMediaEncrypted {
Button {
callViewModel.showZrtpSasDialogIfPossible()
} label: {
Image(callViewModel.isZrtpPq ? "media-encryption-zrtp-pq" : "media-encryption-srtp")
.resizable()
.frame(width: 30, height: 30)
.padding(.horizontal)
}
}
if telecomManager.remoteVideo {
Button {
callViewModel.switchCamera()
@ -263,50 +283,71 @@ struct CallView: View {
ZStack {
VStack {
Spacer()
if callViewModel.remoteAddress != nil {
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
ZStack {
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
if callViewModel.isRemoteDeviceTrusted {
Circle()
.fill(Color.blueInfo500)
.frame(width: 105, height: 105)
}
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true)
if callViewModel.remoteAddress != nil {
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true)
}
} else {
if callViewModel.remoteAddress!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.displayName!,
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}
} else {
if callViewModel.remoteAddress!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.displayName!,
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}
} else {
Image("profil-picture-default")
.resizable()
if callViewModel.isRemoteDeviceTrusted {
VStack {
Spacer()
HStack {
Image("trusted")
.resizable()
.frame(width: 25, height: 25)
Spacer()
}
}
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}
Text(callViewModel.displayName)

View file

@ -0,0 +1,180 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import Foundation
struct ZRTPPopup: View {
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var callViewModel: CallViewModel
@State private var letters1: String = "AA"
@State private var letters2: String = "BB"
@State private var letters3: String = "CC"
@State private var letters4: String = "DD"
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("Validate the device")
.default_text_style_600(styleSize: 20)
Text("Say \(callViewModel.upperCaseAuthTokenToRead) and click on the letters given by your correspondent:")
.default_text_style(styleSize: 15)
.padding(.bottom, 20)
HStack(spacing: 25) {
Spacer()
HStack(alignment: .center) {
Text(letters1)
.default_text_style(styleSize: 30)
.frame(width: 60, height: 60)
}
.padding(10)
.background(Color.grayMain2c200)
.cornerRadius(40)
.onTapGesture {
callViewModel.lettersClicked(letters: letters1)
callViewModel.zrtpPopupDisplayed = false
}
HStack(alignment: .center) {
Text(letters2)
.default_text_style(styleSize: 30)
.frame(width: 60, height: 60)
}
.padding(10)
.background(Color.grayMain2c200)
.cornerRadius(40)
.onTapGesture {
callViewModel.lettersClicked(letters: letters2)
callViewModel.zrtpPopupDisplayed = false
}
Spacer()
}
.padding(.bottom, 20)
HStack(spacing: 25) {
Spacer()
HStack(alignment: .center) {
Text(letters3)
.default_text_style(styleSize: 30)
.frame(width: 60, height: 60)
}
.padding(10)
.background(Color.grayMain2c200)
.cornerRadius(40)
.onTapGesture {
callViewModel.lettersClicked(letters: letters3)
callViewModel.zrtpPopupDisplayed = false
}
HStack(alignment: .center) {
Text(letters4)
.default_text_style(styleSize: 30)
.frame(width: 60, height: 60)
}
.padding(10)
.background(Color.grayMain2c200)
.cornerRadius(40)
.onTapGesture {
callViewModel.lettersClicked(letters: letters4)
callViewModel.zrtpPopupDisplayed = false
}
Spacer()
}
.padding(.bottom, 20)
HStack {
Text("Skip")
.underline()
.tint(Color.grayMain2c600)
.default_text_style_600(styleSize: 15)
.foregroundStyle(Color.grayMain2c500)
}
.frame(maxWidth: .infinity)
.padding(.bottom, 30)
.onTapGesture {
callViewModel.zrtpPopupDisplayed = false
}
Button(action: {
callViewModel.zrtpPopupDisplayed = false
}, label: {
Text("Letters don't match!")
.default_text_style_orange_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(Color.orangeMain500, lineWidth: 1)
)
.padding(.bottom)
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
.background(.white)
.cornerRadius(20)
.padding(.horizontal)
.frame(maxHeight: .infinity)
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
.frame(maxWidth: sharedMainViewModel.maxWidth)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
.onAppear {
var random = SystemRandomNumberGenerator()
let correctLetters = Int(random.next(upperBound: UInt32(4)))
letters1 = (correctLetters == 0) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
letters2 = (correctLetters == 1) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
letters3 = (correctLetters == 2) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
letters4 = (correctLetters == 3) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
}
}
}
func randomAlphanumericString(_ length: Int) -> String {
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let len = UInt32(letters.count)
var random = SystemRandomNumberGenerator()
var randomString = ""
for _ in 0..<length {
let randomIndex = Int(random.next(upperBound: len))
let randomCharacter = letters[letters.index(letters.startIndex, offsetBy: randomIndex)]
randomString.append(randomCharacter)
}
return randomString
}
}
#Preview {
ZRTPPopup(callViewModel: CallViewModel())
}

View file

@ -20,6 +20,7 @@
import SwiftUI
import linphonesw
import AVFAudio
import Combine
class CallViewModel: ObservableObject {
@ -36,11 +37,19 @@ class CallViewModel: ObservableObject {
@Published var isRemoteRecording: Bool = false
@Published var isPaused: Bool = false
@Published var timeElapsed: Int = 0
@Published var zrtpPopupDisplayed: Bool = false
@Published var upperCaseAuthTokenToRead = ""
@Published var upperCaseAuthTokenToListen = ""
@Published var isMediaEncrypted: Bool = false
@Published var isZrtpPq: Bool = false
@Published var isRemoteDeviceTrusted: Bool = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var currentCall: Call?
private var callSuscriptions = Set<AnyCancellable?>()
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
@ -78,6 +87,10 @@ class CallViewModel: ObservableObject {
self.isPaused = self.isCallPaused()
self.timeElapsed = 0
}
self.callSuscriptions.insert(self.currentCall!.publisher?.onEncryptionChanged?.postOnMainQueue {(cbVal: (call: Call, on: Bool, authenticationToken: String?)) in
_ = self.updateEncryption()
})
}
}
}
@ -268,4 +281,105 @@ class CallViewModel: ObservableObject {
}
}
}
func lettersClicked(letters: String) {
let verified = letters == self.upperCaseAuthTokenToListen
Log.info(
"[ZRTPPopup] User clicked on \(verified ? "right" : "wrong") letters"
)
if verified {
coreContext.doOnCoreQueue { core in
if core.currentCall != nil {
core.currentCall!.authenticationTokenVerified = verified
}
}
}
}
private func updateEncryption() -> Bool {
if currentCall != nil && currentCall!.currentParams != nil {
switch currentCall!.currentParams!.mediaEncryption {
case MediaEncryption.ZRTP:
let authToken = currentCall!.authenticationToken
let isDeviceTrusted = currentCall!.authenticationTokenVerified && authToken != nil
Log.info(
"[CallViewModel] Current call media encryption is ZRTP, auth token is \(isDeviceTrusted ? "trusted" : "not trusted yet")"
)
isRemoteDeviceTrusted = isDeviceTrusted
if isDeviceTrusted {
ToastViewModel.shared.toastMessage = "Info_call_securised"
ToastViewModel.shared.displayToast = true
}
/*
let securityLevel = isDeviceTrusted ? SecurityLevel.Safe : SecurityLevel.Encrypted
let avatarModel = contact
if (avatarModel != nil) {
avatarModel.trust.postValue(securityLevel)
contact.postValue(avatarModel!!)
} else {
Log.error("$TAG No avatar model found!")
}
*/
isMediaEncrypted = true
// When Post Quantum is available, ZRTP is Post Quantum
isZrtpPq = Core.getPostQuantumAvailable
if !isDeviceTrusted && authToken != nil && !authToken!.isEmpty {
Log.info("[CallViewModel] Showing ZRTP SAS confirmation dialog")
showZrtpSasDialog(authToken: authToken!)
}
return isDeviceTrusted
case MediaEncryption.SRTP, MediaEncryption.DTLS:
isMediaEncrypted = true
isZrtpPq = false
return false
default:
isMediaEncrypted = false
isZrtpPq = false
return false
}
}
return false
}
func showZrtpSasDialogIfPossible() {
if currentCall != nil && currentCall!.currentParams != nil && currentCall!.currentParams!.mediaEncryption == MediaEncryption.ZRTP {
let authToken = currentCall!.authenticationToken
let isDeviceTrusted = currentCall!.authenticationTokenVerified && authToken != nil
Log.info(
"[CallViewModel] Current call media encryption is ZRTP, auth token is \(isDeviceTrusted ? "trusted" : "not trusted yet")"
)
if (authToken != nil && !authToken!.isEmpty) {
showZrtpSasDialog(authToken: authToken!)
}
}
}
private func showZrtpSasDialog(authToken: String) {
if self.currentCall != nil {
let upperCaseAuthToken = authToken.localizedUppercase
let mySubstringPrefix = upperCaseAuthToken.prefix(2)
let mySubstringSuffix = upperCaseAuthToken.suffix(2)
switch self.currentCall!.dir {
case Call.Dir.Incoming:
self.upperCaseAuthTokenToRead = String(mySubstringPrefix)
self.upperCaseAuthTokenToListen = String(mySubstringSuffix)
default:
self.upperCaseAuthTokenToRead = String(mySubstringSuffix)
self.upperCaseAuthTokenToListen = String(mySubstringPrefix)
}
self.zrtpPopupDisplayed = true
}
}
}

View file

@ -27,11 +27,17 @@ struct ToastView: View {
VStack {
if toastViewModel.displayToast {
HStack {
Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle")
.resizable()
.renderingMode(.template)
.frame(width: 25, height: 25, alignment: .leading)
.foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
if toastViewModel.toastMessage.contains("Info_") {
Image("trusted")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
} else {
Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle")
.resizable()
.renderingMode(.template)
.frame(width: 25, height: 25, alignment: .leading)
.foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
}
switch toastViewModel.toastMessage {
case "Successful":
@ -68,7 +74,14 @@ struct ToastView: View {
.foregroundStyle(Color.greenSuccess500)
.default_text_style(styleSize: 15)
.padding(8)
case "Info_call_securised":
Text("This call is completely securised")
.multilineTextAlignment(.center)
.foregroundStyle(Color.blueInfo500)
.default_text_style(styleSize: 15)
.padding(8)
case let str where str.contains("is recording"):
Text(toastViewModel.toastMessage)
.multilineTextAlignment(.center)
@ -111,7 +124,7 @@ struct ToastView: View {
.overlay(
RoundedRectangle(cornerRadius: 50)
.inset(by: 0.5)
.stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1)
.stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : (toastViewModel.toastMessage.contains("Info_") ? Color.blueInfo500 : Color.redDanger500), lineWidth: 1)
)
.onTapGesture {
if !toastViewModel.toastMessage.contains("is recording") {

View file

@ -1,33 +1,45 @@
//
// ActivityIndicator.swift
// Linphone
//
// Created by Martins Benoît on 13/12/2023.
//
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
struct ActivityIndicator: View {
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
@State var animate = false
let color1 = Color.white
let color2 = Color.white.opacity(0.5)
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
.rotationEffect(Angle(degrees: animate ? 360: 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
}.onAppear {
self.animate.toggle()
}
}
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
@State var animate = false
let color1 = Color.white
let color2 = Color.white.opacity(0.5)
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
.rotationEffect(Angle(degrees: animate ? 360: 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
}.onAppear {
self.animate.toggle()
}
}
}
#Preview {
ActivityIndicator()
ActivityIndicator()
}