diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index fe58d02a7..6e6a82566 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -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 = ""; }; D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = ""; }; + D75759312B56D40900E7AC10 /* ZRTPPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZRTPPopup.swift; sourceTree = ""; }; D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; @@ -401,6 +403,14 @@ path = Fragments; sourceTree = ""; }; + D75759302B56D3CE00E7AC10 /* Fragments */ = { + isa = PBXGroup; + children = ( + D75759312B56D40900E7AC10 /* ZRTPPopup.swift */, + ); + path = Fragments; + sourceTree = ""; + }; 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 */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index f2c8a82a2..6f743354f 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -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 diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 865f0fc3e..3064c318a 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -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" : { diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index d31ed8b4d..a80beb43c 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -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 diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 58f8fe232..3e302e8b7 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -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) diff --git a/Linphone/UI/Call/Fragments/ZRTPPopup.swift b/Linphone/UI/Call/Fragments/ZRTPPopup.swift new file mode 100644 index 000000000..8e1a1b448 --- /dev/null +++ b/Linphone/UI/Call/Fragments/ZRTPPopup.swift @@ -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 . + */ + +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..() + 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 + } + } } diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index 567be383a..67e01a6b4 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -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") { diff --git a/Linphone/Utils/ActivityIndicator.swift b/Linphone/Utils/ActivityIndicator.swift index 64be15f74..862ff4ed6 100644 --- a/Linphone/Utils/ActivityIndicator.swift +++ b/Linphone/Utils/ActivityIndicator.swift @@ -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 . + */ 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() }