From d5d1600b4e4202dacc90cae3a9c4f61b60a951db Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 12 Feb 2026 17:58:16 +0100 Subject: [PATCH] Add trusted devices list --- .../arrow-right.imageset/Contents.json | 21 + .../arrow-right.imageset/arrow-right.svg | 1 + Linphone/GeneratedGit.swift | 4 +- .../Localizable/en.lproj/Localizable.strings | 13 +- .../Localizable/fr.lproj/Localizable.strings | 13 +- .../Contacts/Fragments/ContactFragment.swift | 14 +- .../ContactInnerActionsFragment.swift | 198 ++++++++- .../Fragments/ContactInnerFragment.swift | 414 +++++++++--------- .../Contacts/Model/ContactDeviceModel.swift | 34 ++ .../ViewModel/ContactsListViewModel.swift | 75 +++- Linphone/UI/Main/ContentView.swift | 130 ++++++ .../Fragments/ChatBubbleView.swift | 2 +- Linphone/UI/Main/Fragments/PopupView.swift | 43 +- .../Main/Viewmodel/SharedMainViewModel.swift | 18 + LinphoneApp.xcodeproj/project.pbxproj | 4 + 15 files changed, 729 insertions(+), 255 deletions(-) create mode 100644 Linphone/Assets.xcassets/arrow-right.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/arrow-right.imageset/arrow-right.svg create mode 100644 Linphone/UI/Main/Contacts/Model/ContactDeviceModel.swift diff --git a/Linphone/Assets.xcassets/arrow-right.imageset/Contents.json b/Linphone/Assets.xcassets/arrow-right.imageset/Contents.json new file mode 100644 index 000000000..e32a0420f --- /dev/null +++ b/Linphone/Assets.xcassets/arrow-right.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "arrow-right.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/arrow-right.imageset/arrow-right.svg b/Linphone/Assets.xcassets/arrow-right.imageset/arrow-right.svg new file mode 100644 index 000000000..39e1452e8 --- /dev/null +++ b/Linphone/Assets.xcassets/arrow-right.imageset/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift index 757db7c38..213b92b88 100644 --- a/Linphone/GeneratedGit.swift +++ b/Linphone/GeneratedGit.swift @@ -1,7 +1,7 @@ import Foundation public enum AppGitInfo { - public static let branch = "master" - public static let commit = "304651d77" + public static let branch = "feature/trust_list" + public static let commit = "692a308d7" public static let tag = "6.1.0-alpha" } diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 1eaa0e7fe..a8a1480cd 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -172,8 +172,15 @@ "contact_details_numbers_and_addresses_title" = "Phone numbers & SIP addresses"; "contact_details_remove_from_favourites" = "Remove from favourites"; "contact_details_share" = "Share"; -"contact_dialog_delete_message" = "This contact will be definitively removed."; +"contact_details_trust_title" = "Trust"; +"contact_details_no_device_found" = "No device found…"; +"contact_details_trusted_devices_count" = "Number of trusted devices:"; +"contact_dialog_increase_trust_level_title" = "Increase trust level"; +"contact_dialog_increase_trust_level_message" = "You're about to make a call to %1$@'s device %2$@.\nDo you want to make the call?"; +"contact_dialog_devices_trust_help_title" = "Trust level"; +"contact_dialog_devices_trust_help_message" = "Check all of your contact devices to make sure your communications will be secured an unaltered.\nWhen all will be verified, you'll reach maximum trust level."; "contact_dialog_delete_title" = "Delete %@?"; +"contact_dialog_delete_message" = "This contact will be definitively removed."; "contact_dialog_pick_phone_number_or_sip_address_title" = "Choose a number or a SIP address"; "contact_edit_title" = "Edit contact"; "contact_editor_company" = "Company"; @@ -182,6 +189,8 @@ "contact_editor_first_name" = "First name"; "contact_editor_job_title" = "Job title"; "contact_editor_last_name" = "Last name"; +"contact_make_call_check_device_trust" = "Verify"; +"contact_device_without_name" = "Unnamed device"; "contact_message_action" = "Message"; "contact_new_title" = "New contact"; "contact_video_call_action" = "Video call"; @@ -288,8 +297,10 @@ "dialog_continue" = "Continue"; "dialog_deny" = "Deny"; "dialog_install" = "Install"; +"dialog_do_not_show_anymore" = "Do not show this dialog anymore"; "dialog_no" = "No"; "dialog_confirm" = "Confirm"; +"dialog_understood" = "Understood"; "dialog_yes" = "Yes"; "drawer_menu_account_connection_status_cleared" = "Disabled"; "drawer_menu_account_connection_status_connected" = "Connected"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 0d87159c3..c9ab342bb 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -172,8 +172,15 @@ "contact_details_numbers_and_addresses_title" = "Coordonnées"; "contact_details_remove_from_favourites" = "Retirer des favoris"; "contact_details_share" = "Partager"; -"contact_dialog_delete_message" = "Ce contact sera définitivement supprimé."; +"contact_details_trust_title" = "Confiance"; +"contact_details_no_device_found" = "Aucun appareil trouvé"; +"contact_details_trusted_devices_count" = "Appareils du contact :"; +"contact_dialog_increase_trust_level_title" = "Vérifier l'appareil ?"; +"contact_dialog_increase_trust_level_message" = "Voulez-vous appeler l’appareil %2$@ de %1$@ ?\nVoulez-vous passer l’appel ?"; +"contact_dialog_devices_trust_help_title" = "Niveau de confiance"; +"contact_dialog_devices_trust_help_message" = "Vérifiez les appareils de votre contact pour confirmer que vos communications seront sécurisées et sans compromission.\nQuand tous seront vérifiés, vous atteindrez le niveau de confiance maximal."; "contact_dialog_delete_title" = "Supprimer %@ ?"; +"contact_dialog_delete_message" = "Ce contact sera définitivement supprimé."; "contact_dialog_pick_phone_number_or_sip_address_title" = "Choisissez un numéro ou adresse SIP"; "contact_edit_title" = "Modifier contact"; "contact_editor_company" = "Entreprise"; @@ -182,6 +189,8 @@ "contact_editor_first_name" = "Prénom"; "contact_editor_job_title" = "Poste"; "contact_editor_last_name" = "Nom de famille"; +"contact_make_call_check_device_trust" = "Vérifier"; +"contact_device_without_name" = "Appareil sans nom"; "contact_message_action" = "Message"; "contact_new_title" = "Nouveau contact"; "contact_video_call_action" = "Appel vidéo"; @@ -288,8 +297,10 @@ "dialog_continue" = "Continuer"; "dialog_deny" = "Refuser"; "dialog_install" = "Installer"; +"dialog_do_not_show_anymore" = "Ne plus me montrer ce message"; "dialog_no" = "Non"; "dialog_confirm" = "Confirmer"; +"dialog_understood" = "J'ai compris"; "dialog_yes" = "Oui"; "drawer_menu_account_connection_status_cleared" = "Désactivé"; "drawer_menu_account_connection_status_connected" = "Connecté"; diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index 10ec4c7ca..f61119f9b 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -29,8 +29,10 @@ struct ContactFragment: View { @Binding var isShowDeletePopup: Bool @Binding var isShowDismissPopup: Bool + @Binding var isShowTrustLevelPopup: Bool @Binding var isShowSipAddressesPopup: Bool @Binding var isShowSipAddressesPopupType: Int + @Binding var isShowIncreaseTrustLevelPopup: Bool @Binding var isShowEditContactFragmentInContactDetails: Bool @State private var showingSheet = false @@ -68,19 +70,11 @@ struct ContactFragment: View { showingSheet: $showingSheet, showShareSheet: $showShareSheet, isShowDismissPopup: $isShowDismissPopup, + isShowTrustLevelPopup: $isShowTrustLevelPopup, isShowSipAddressesPopup: $isShowSipAddressesPopup, isShowSipAddressesPopupType: $isShowSipAddressesPopupType, + isShowIncreaseTrustLevelPopup: $isShowIncreaseTrustLevelPopup, isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails ) } } - -#Preview { - ContactFragment( - isShowDeletePopup: .constant(false), - isShowDismissPopup: .constant(false), - isShowSipAddressesPopup: .constant(false), - isShowSipAddressesPopupType: .constant(0), - isShowEditContactFragmentInContactDetails: .constant(false) - ) -} diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index d411ed26c..0b3182e4d 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -29,14 +29,19 @@ struct ContactInnerActionsFragment: View { @EnvironmentObject var contactAvatarModel: ContactAvatarModel @EnvironmentObject var contactsListViewModel: ContactsListViewModel + @State private var trustIsOpen = true @State private var informationIsOpen = true @Binding var showingSheet: Bool @Binding var showShareSheet: Bool @Binding var isShowDeletePopup: Bool @Binding var isShowDismissPopup: Bool + @Binding var isShowTrustLevelPopup: Bool + @Binding var isShowIncreaseTrustLevelPopup: Bool @Binding var isShowEditContactFragmentInContactDetails: Bool + let geometry: GeometryProxy + var actionEditButton: () -> Void var body: some View { @@ -180,12 +185,14 @@ struct ContactInnerActionsFragment: View { .padding(.horizontal) .zIndex(-1) .transition(.move(edge: .top)) + .background(Color.gray100) } } else { HStack {} .frame(height: 20) } + if !contactAvatarModel.organization.isEmpty || !contactAvatarModel.jobTitle.isEmpty { VStack { if !contactAvatarModel.organization.isEmpty { @@ -213,17 +220,188 @@ struct ContactInnerActionsFragment: View { .transition(.move(edge: .top)) } - // TODO Trust Fragment + HStack(alignment: .center) { + Button { + isShowTrustLevelPopup = true + } label: { + HStack { + Text("contact_details_trust_title") + .default_text_style_800(styleSize: 15) + + Image("question") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 22, height: 22) + } + } + + Spacer() + + Image(trustIsOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + .background(Color.gray100) + .onTapGesture { + withAnimation { + trustIsOpen.toggle() + } + } - // TODO Medias Fragment + if trustIsOpen { + VStack(spacing: 0) { + if !contactsListViewModel.devices.isEmpty { + Text("contact_details_trusted_devices_count") + .default_text_style_700(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 20) + .padding(.horizontal, 20) + .padding(.bottom, 10) + + let radius = geometry.size.height * 0.5 + let barWidth = min(geometry.size.width - 70, SharedMainViewModel.shared.maxWidth - 70) + + ZStack(alignment: .leading) { + Rectangle() + .foregroundColor(Color.blueInfo500.opacity(0.2)) + .frame(width: barWidth, height: 30) + .clipShape(RoundedRectangle(cornerRadius: radius)) + + if contactsListViewModel.trustedDevicesPercentage >= 15 { + Rectangle() + .foregroundColor(Color.blueInfo500) + .frame(width: ((contactsListViewModel.trustedDevicesPercentage / 100) * barWidth) - 6, height: 25) + .clipShape(RoundedRectangle(cornerRadius: radius)) + .padding(.horizontal, 3) + } else if contactsListViewModel.trustedDevicesPercentage > 0 { + Rectangle() + .foregroundColor(Color.blueInfo500) + .frame(width: ((10 / 100) * barWidth) - 6, height: 25) + .clipShape(RoundedRectangle(cornerRadius: radius)) + .padding(.horizontal, 3) + } + + if contactsListViewModel.trustedDevicesPercentage >= 30 { + Text(String(Int(contactsListViewModel.trustedDevicesPercentage)) + "%") + .default_text_style_white_700(styleSize: 14) + .frame(width: (contactsListViewModel.trustedDevicesPercentage / 100) * barWidth, height: 25, alignment: .center) + } else { + Text(String(Int(contactsListViewModel.trustedDevicesPercentage)) + "%") + .foregroundStyle(contactsListViewModel.trustedDevicesPercentage == 0 ? Color.redDanger500 : Color.blueInfo500) + .default_text_style_white_700(styleSize: 14) + .frame(width: barWidth, height: 25, alignment: .center) + } + } + .frame(width: barWidth, height: 30) + .contentShape(Rectangle()) + .padding(.bottom, 10) + + ForEach(contactsListViewModel.devices) { device in + HStack { + Text(device.name) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack { + if !device.trusted { + Button { + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceName = device.name + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceAddress = device.address + isShowIncreaseTrustLevelPopup = true + } label: { + HStack { + Image("warning-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25) + .padding(.all, 6) + + Text("contact_make_call_check_device_trust") + .foregroundStyle(Color.orangeMain500) + .default_text_style(styleSize: 14) + .lineLimit(1) + .padding(.leading, -5) + .padding(.trailing, 15) + } + } + .background(Color.orangeMain100) + .cornerRadius(25) + } else { + ZStack { + Button { + } label: { + HStack { + Image("warning-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25) + .padding(.all, 6) + + Text("contact_make_call_check_device_trust") + .foregroundStyle(Color.orangeMain500) + .default_text_style(styleSize: 14) + .lineLimit(1) + .padding(.leading, -5) + .padding(.trailing, 15) + } + } + .background(Color.orangeMain100) + .cornerRadius(25) + .hidden() + + Image("trusted") + .resizable() + .frame(width: 28, height: 28) + } + } + } + .frame(height: 40) + } + .background(.white) + .padding(.vertical, 10) + .padding(.horizontal, 20) + } + } else { + Text("contact_details_no_device_found") + .default_text_style_700(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 15) + .padding(.horizontal, 20) + .padding(.bottom, 10) + } + } + .padding(.bottom, 5) + .background(.white) + .cornerRadius(15) + .padding(.horizontal) + .zIndex(-2) + .transition(.move(edge: .top)) + } HStack(alignment: .center) { Text("contact_details_actions_title") .default_text_style_800(styleSize: 16) Spacer() + + Image("caret-up") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .hidden() } - .padding(.vertical, 10) + .padding(.top, 20) + .padding(.bottom, 10) .padding(.horizontal, 16) .background(Color.gray100) @@ -372,18 +550,6 @@ struct ContactInnerActionsFragment: View { .padding(.horizontal) .zIndex(-1) .transition(.move(edge: .top)) - } + } } - -#Preview { - ContactInnerActionsFragment( - showingSheet: .constant(false), - showShareSheet: .constant(false), - isShowDeletePopup: .constant(false), - isShowDismissPopup: .constant(false), - isShowEditContactFragmentInContactDetails: .constant(false), - actionEditButton: {} - ) -} - // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index e58345846..90cb4d3e7 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -39,57 +39,47 @@ struct ContactInnerFragment: View { @Binding var showingSheet: Bool @Binding var showShareSheet: Bool @Binding var isShowDismissPopup: Bool + @Binding var isShowTrustLevelPopup: Bool @Binding var isShowSipAddressesPopup: Bool @Binding var isShowSipAddressesPopupType: Int + @Binding var isShowIncreaseTrustLevelPopup: Bool @Binding var isShowEditContactFragmentInContactDetails: Bool var body: some View { NavigationView { - ZStack { - VStack(spacing: 1) { - Rectangle() - .foregroundColor(Color.orangeMain500) - .edgesIgnoringSafeArea(.top) - .frame(height: 0) - - HStack { - if !(orientation == .landscapeLeft || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { - Image("caret-left") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - .padding(.top, 2) - .padding(.leading, -10) - .onTapGesture { - withAnimation { - SharedMainViewModel.shared.displayedFriend = nil + GeometryReader { geometry in + ZStack { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + if !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 2) + .padding(.leading, -10) + .onTapGesture { + withAnimation { + SharedMainViewModel.shared.displayedFriend = nil + } } - } - } - - Spacer() - - if !contactAvatarModel.isReadOnly { - if !contactAvatarModel.editable { - Button(action: { - editNativeContact() - }, label: { - Image("pencil-simple") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - .padding(.top, 2) - }) - } else { - NavigationLink(destination: EditContactFragment( - contactAvatarModel: contactAvatarModel, - isShowEditContactFragment: $isShowEditContactFragmentInContactDetails, - isShowDismissPopup: $isShowDismissPopup)) { + } + + Spacer() + + if !contactAvatarModel.isReadOnly { + if !contactAvatarModel.editable { + Button(action: { + editNativeContact() + }, label: { Image("pencil-simple") .renderingMode(.template) .resizable() @@ -97,151 +87,81 @@ struct ContactInnerFragment: View { .frame(width: 25, height: 25, alignment: .leading) .padding(.all, 10) .padding(.top, 2) - } - .simultaneousGesture( - TapGesture().onEnded { - isShowEditContactFragmentInContactDetails = true + }) + } else { + NavigationLink(destination: EditContactFragment( + contactAvatarModel: contactAvatarModel, + isShowEditContactFragment: $isShowEditContactFragmentInContactDetails, + isShowDismissPopup: $isShowDismissPopup)) { + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 2) } - ) + .simultaneousGesture( + TapGesture().onEnded { + isShowEditContactFragmentInContactDetails = true + } + ) + } } } - } - .frame(maxWidth: .infinity) - .frame(height: 50) - .padding(.horizontal) - .padding(.bottom, 4) - .background(.white) - - ScrollView { - VStack(spacing: 0) { + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { VStack(spacing: 0) { VStack(spacing: 0) { - if SharedMainViewModel.shared.displayedFriend != nil { - Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100) - - Text(contactAvatarModel.name) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 10) - - Text(contactAvatarModel.lastPresenceInfo) - .foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online" - ? Color.greenSuccess500 - : Color.orangeWarning600) - .multilineTextAlignment(.center) - .default_text_style_300(styleSize: 12) - .frame(maxWidth: .infinity) - } - } - .frame(minHeight: 150) - .frame(maxWidth: .infinity) - .padding(.top, 10) - .background(Color.gray100) - - HStack { - Spacer() - - Button(action: { - CoreContext.shared.doOnCoreQueue { core in - if contactAvatarModel.addresses.count == 1 { - do { - let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address) - telecomManager.doCallOrJoinConf(address: address, isVideo: false) - } catch { - Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") - } - } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { - if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { - telecomManager.doCallOrJoinConf(address: address, isVideo: false) - } - } else { - DispatchQueue.main.async { - isShowSipAddressesPopupType = 0 - isShowSipAddressesPopup = true - } - } - } - }, label: { - VStack { - HStack(alignment: .center) { - Image("phone") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25) - } - .padding(16) - .background(Color.grayMain2c200) - .cornerRadius(40) + VStack(spacing: 0) { + if SharedMainViewModel.shared.displayedFriend != nil { + Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100) - Text("contact_call_action") + Text(contactAvatarModel.name) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text(contactAvatarModel.lastPresenceInfo) + .foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online" + ? Color.greenSuccess500 + : Color.orangeWarning600) + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) } - }) - - if !AppServices.corePreferences.disableChatFeature { - Spacer() - - Button(action: { - CoreContext.shared.doOnCoreQueue { core in - if contactAvatarModel.addresses.count == 1 { - do { - let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address) - contactsListViewModel.createOneToOneChatRoomWith(remote: address) - } catch { - Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") - } - } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { - if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { - contactsListViewModel.createOneToOneChatRoomWith(remote: address) - } - } else { - DispatchQueue.main.async { - isShowSipAddressesPopupType = 1 - isShowSipAddressesPopup = true - } - } - } - }, label: { - VStack { - HStack(alignment: .center) { - Image("chat-teardrop-text") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25) - } - .padding(16) - .background(Color.grayMain2c200) - .cornerRadius(40) - - Text("contact_message_action") - .default_text_style(styleSize: 14) - } - }) - } - - Spacer() + } + .frame(minHeight: 150) + .frame(maxWidth: .infinity) + .padding(.top, 10) + .background(Color.gray100) - if !SharedMainViewModel.shared.disableVideoCall { + HStack { + Spacer() + Button(action: { CoreContext.shared.doOnCoreQueue { core in if contactAvatarModel.addresses.count == 1 { do { let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address) - telecomManager.doCallOrJoinConf(address: address, isVideo: true) + telecomManager.doCallOrJoinConf(address: address, isVideo: false) } catch { Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") } } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { - telecomManager.doCallOrJoinConf(address: address, isVideo: true) + telecomManager.doCallOrJoinConf(address: address, isVideo: false) } } else { DispatchQueue.main.async { - isShowSipAddressesPopupType = 2 + isShowSipAddressesPopupType = 0 isShowSipAddressesPopup = true } } @@ -249,7 +169,7 @@ struct ContactInnerFragment: View { }, label: { VStack { HStack(alignment: .center) { - Image("video-camera") + Image("phone") .renderingMode(.template) .resizable() .foregroundStyle(Color.grayMain2c600) @@ -259,44 +179,134 @@ struct ContactInnerFragment: View { .background(Color.grayMain2c200) .cornerRadius(40) - Text("contact_video_call_action") + Text("contact_call_action") .default_text_style(styleSize: 14) } }) + if !AppServices.corePreferences.disableChatFeature { + Spacer() + + Button(action: { + CoreContext.shared.doOnCoreQueue { core in + if contactAvatarModel.addresses.count == 1 { + do { + let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address) + contactsListViewModel.createOneToOneChatRoomWith(remote: address) + } catch { + Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") + } + } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { + if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { + contactsListViewModel.createOneToOneChatRoomWith(remote: address) + } + } else { + DispatchQueue.main.async { + isShowSipAddressesPopupType = 1 + isShowSipAddressesPopup = true + } + } + } + }, label: { + VStack { + HStack(alignment: .center) { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("contact_message_action") + .default_text_style(styleSize: 14) + } + }) + } + Spacer() + + if !SharedMainViewModel.shared.disableVideoCall { + Button(action: { + CoreContext.shared.doOnCoreQueue { core in + if contactAvatarModel.addresses.count == 1 { + do { + let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address) + telecomManager.doCallOrJoinConf(address: address, isVideo: true) + } catch { + Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") + } + } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { + if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { + telecomManager.doCallOrJoinConf(address: address, isVideo: true) + } + } else { + DispatchQueue.main.async { + isShowSipAddressesPopupType = 2 + isShowSipAddressesPopup = true + } + } + } + }, label: { + VStack { + HStack(alignment: .center) { + Image("video-camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("contact_video_call_action") + .default_text_style(styleSize: 14) + } + }) + + Spacer() + } + } + .padding(.top, 20) + .frame(maxWidth: .infinity) + .background(Color.gray100) + + ContactInnerActionsFragment( + showingSheet: $showingSheet, + showShareSheet: $showShareSheet, + isShowDeletePopup: $isShowDeletePopup, + isShowDismissPopup: $isShowDismissPopup, + isShowTrustLevelPopup: $isShowTrustLevelPopup, + isShowIncreaseTrustLevelPopup: $isShowIncreaseTrustLevelPopup, + isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails, + geometry: geometry, + actionEditButton: editNativeContact + ) + .onAppear { + contactsListViewModel.fetchDevicesAndTrust() } } - .padding(.top, 20) - .frame(maxWidth: .infinity) - .background(Color.gray100) - - ContactInnerActionsFragment( - showingSheet: $showingSheet, - showShareSheet: $showShareSheet, - isShowDeletePopup: $isShowDeletePopup, - isShowDismissPopup: $isShowDismissPopup, - isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails, - actionEditButton: editNativeContact - ) + .frame(maxWidth: SharedMainViewModel.shared.maxWidth) } - .frame(maxWidth: SharedMainViewModel.shared.maxWidth) + .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity) + .background(Color.gray100) } - .background(Color.gray100) - } - .background(.white) - .navigationBarHidden(true) - .onRotate { newOrientation in - orientation = newOrientation - } - .fullScreenCover(isPresented: $presentingEditContact) { - NavigationView { - EditContactView(contact: $cnContact) - .navigationBarTitle("contact_edit_title") - .navigationBarTitleDisplayMode(.inline) - .edgesIgnoringSafeArea(.vertical) + .background(.white) + .navigationBarHidden(true) + .onRotate { newOrientation in + orientation = newOrientation + } + .fullScreenCover(isPresented: $presentingEditContact) { + NavigationView { + EditContactView(contact: $cnContact) + .navigationBarTitle("contact_edit_title") + .navigationBarTitleDisplayMode(.inline) + .edgesIgnoringSafeArea(.vertical) + } } } } @@ -321,15 +331,3 @@ struct ContactInnerFragment: View { } } } - -#Preview { - ContactInnerFragment( - isShowDeletePopup: .constant(false), - showingSheet: .constant(false), - showShareSheet: .constant(false), - isShowDismissPopup: .constant(false), - isShowSipAddressesPopup: .constant(false), - isShowSipAddressesPopupType: .constant(0), - isShowEditContactFragmentInContactDetails: .constant(false) - ) -} diff --git a/Linphone/UI/Main/Contacts/Model/ContactDeviceModel.swift b/Linphone/UI/Main/Contacts/Model/ContactDeviceModel.swift new file mode 100644 index 000000000..29c40a4e7 --- /dev/null +++ b/Linphone/UI/Main/Contacts/Model/ContactDeviceModel.swift @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of Linphone + * + * 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 Foundation +import linphonesw + +class ContactDeviceModel: ObservableObject, Identifiable { + let id = UUID() + var name: String + var address: Address + var trusted: Bool + + init(name: String, address: Address, trusted: Bool) { + self.name = name + self.address = address + self.trusted = trusted + } +} diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift index fa7a74983..c23f4b178 100644 --- a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift @@ -23,6 +23,8 @@ import SwiftUI // swiftlint:disable line_length class ContactsListViewModel: ObservableObject { + static let TAG = "[ConversationForwardMessageViewModel]" + @Published var selectedEditFriend: ContactAvatarModel? var stringToCopy: String = "" @@ -31,22 +33,28 @@ class ContactsListViewModel: ObservableObject { var selectedFriendToShare: ContactAvatarModel? var selectedFriendToDelete: ContactAvatarModel? + @Published var devices: [ContactDeviceModel] = [] + @Published var trustedDevicesPercentage: Double = 0.0 + @Published var displayedConversation: ConversationModel? + private var coreDelegate: CoreDelegate? private var contactChatRoomDelegate: ChatRoomDelegate? private let nativeAddressBookFriendList = "Native address-book" let linphoneAddressBookFriendList = "Linphone address-book" let tempRemoteAddressBookFriendList = "TempRemoteDirectoryContacts address-book" - init() {} + init() { + addCoreDelegate() + } func createOneToOneChatRoomWith(remote: Address) { CoreContext.shared.doOnCoreQueue { core in let account = core.defaultAccount if account == nil { Log.error( - "\(ConversationForwardMessageViewModel.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())" + "\(Self.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())" ) return } @@ -211,6 +219,30 @@ class ContactsListViewModel: ObservableObject { chatRoom.addDelegate(delegate: contactChatRoomDelegate!) } + func addCoreDelegate() { + CoreContext.shared.doOnCoreQueue { core in + if let coreDelegate = self.coreDelegate { + core.removeDelegate(delegate: coreDelegate) + self.coreDelegate = nil + } + + self.coreDelegate = CoreDelegateStub( + onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in + if call.state == Call.State.End && SharedMainViewModel.shared.displayedFriend != nil { + // Updates trust if need be + DispatchQueue.main.async { + self.fetchDevicesAndTrust() + } + } + } + ) + + if self.coreDelegate != nil { + core.addDelegate(delegate: self.coreDelegate!) + } + } + } + func deleteSelectedContact() { CoreContext.shared.doOnCoreQueue { core in if self.selectedFriendToDelete != nil && self.selectedFriendToDelete!.friend != nil { @@ -266,5 +298,44 @@ class ContactsListViewModel: ObservableObject { } } } + + func fetchDevicesAndTrust() { + if let friend = SharedMainViewModel.shared.displayedFriend?.friend { + var devicesList: [ContactDeviceModel] = [] + + let friendDevices = friend.devices + if friendDevices.isEmpty { + Log.info("\(Self.TAG) No device found for friend [\(friend.name ?? "")]") + } else { + let devicesCount = friendDevices.count + var trustedDevicesCount = 0 + + for device in friendDevices { + let trusted = device.securityLevel == .EndToEndEncryptedAndVerified + + if let address = device.address { + devicesList.append( + ContactDeviceModel( + name: device.displayName ?? NSLocalizedString("contact_device_without_name", comment: ""), + address: address, + trusted: trusted + ) + ) + } + + if trusted { + trustedDevicesCount += 1 + } + } + + if !devicesList.isEmpty { + let percentage = trustedDevicesCount * 100 / devicesCount + trustedDevicesPercentage = Double(percentage) + } + } + + devices = devicesList + } + } } // swiftlint:enable line_length diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index f65fc6f4b..c4f01de7b 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -59,6 +59,9 @@ struct ContentView: View { @State var isShowStartConversationFragment = false @State var isShowDismissPopup = false @State var isShowSendCancelMeetingNotificationPopup = false + @State var isShowTrustLevelPopup = false + @State var isShowIncreaseTrustLevelPopup = false + @State var increaseTrustLevelPopupAcceptedTmp = false @State var isShowStartCallGroupPopup = false @State var isShowDeleteMessagePopup = false @State var isShowSipAddressesPopup = false @@ -1013,8 +1016,10 @@ struct ContentView: View { ContactFragment( isShowDeletePopup: $isShowDeleteContactPopup, isShowDismissPopup: $isShowDismissPopup, + isShowTrustLevelPopup: $isShowTrustLevelPopup, isShowSipAddressesPopup: $isShowSipAddressesPopup, isShowSipAddressesPopupType: $isShowSipAddressesPopupType, + isShowIncreaseTrustLevelPopup: $isShowIncreaseTrustLevelPopup, isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails ) .environmentObject(contactsListVM) @@ -1389,6 +1394,131 @@ struct ContentView: View { } } + if isShowTrustLevelPopup { + if let displayedFriend = sharedMainViewModel.displayedFriend { + PopupView( + isShowPopup: $isShowTrustLevelPopup, + title: Text("contact_dialog_devices_trust_help_title"), + content: Text("contact_dialog_devices_trust_help_message"), + additionalContent: { + HStack { + let avatarSize = 50.0 + let avatar = Avatar(contactAvatarModel: displayedFriend, avatarSize: avatarSize, hidePresence: true) + + avatar + + Image("arrow-right") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + + ZStack { + avatar + + Circle() + .stroke(Color.blueInfo500, lineWidth: 2) + .frame(width: avatarSize, height: avatarSize) + + HStack { + VStack { + Spacer() + Image("trusted") + .resizable() + .frame(width: avatarSize/4, height: avatarSize/4) + .padding(.trailing, 1) + .padding(.bottom, 1) + } + Spacer() + } + .frame(width: avatarSize, height: avatarSize) + } + } + .frame(maxWidth: .infinity) + .padding(.bottom, 10) + }, + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_understood"), + actionSecondButton: { self.isShowTrustLevelPopup.toggle() }, + titleThirdButton: nil, + actionThirdButton: {} + ) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowTrustLevelPopup.toggle() + } + } + } + + if isShowIncreaseTrustLevelPopup { + if let displayedFriend = sharedMainViewModel.displayedFriend { + PopupView( + isShowPopup: $isShowIncreaseTrustLevelPopup, + title: Text("contact_dialog_increase_trust_level_title"), + content: Text(String(format: String(localized: "contact_dialog_increase_trust_level_message"), displayedFriend.name, SharedMainViewModel.shared.increaseTrustLevelPopupDeviceName)), + additionalContent: { + if !SharedMainViewModel.shared.increaseTrustLevelPopupAccepted { + HStack { + Button(action: { + increaseTrustLevelPopupAcceptedTmp.toggle() + }, label: { + HStack { + Image(systemName: increaseTrustLevelPopupAcceptedTmp ? "checkmark.square.fill" : "square") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 20, height: 20) + } + }) + .buttonStyle(PlainButtonStyle()) + + Text("dialog_do_not_show_anymore") + .tint(Color.grayMain2c600) + .default_text_style(styleSize: 15) + } + .padding(.bottom, 10) + } + }, + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("contact_call_action"), + actionSecondButton: { + if increaseTrustLevelPopupAcceptedTmp == true { + SharedMainViewModel.shared.changeIncreaseTrustLevelPopupAccepted() + } + + if let deviceAddress = SharedMainViewModel.shared.increaseTrustLevelPopupDeviceAddress { + TelecomManager.shared.doCallOrJoinConf(address: deviceAddress) + } + + self.isShowIncreaseTrustLevelPopup.toggle() + self.increaseTrustLevelPopupAcceptedTmp = false + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceName = "" + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceAddress = nil + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { + self.isShowIncreaseTrustLevelPopup.toggle() + self.increaseTrustLevelPopupAcceptedTmp = false + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceName = "" + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceAddress = nil + + } + ) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowIncreaseTrustLevelPopup.toggle() + self.increaseTrustLevelPopupAcceptedTmp = false + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceName = "" + SharedMainViewModel.shared.increaseTrustLevelPopupDeviceAddress = nil + } + } + } + if isShowStartCallGroupPopup { PopupView( isShowPopup: $isShowStartCallGroupPopup, diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 60d56d0b7..5fbd97a99 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -885,7 +885,7 @@ struct ChatBubbleView: View { } } } - .frame(width: geometryProxy.size.width - 150) + .frame(width: max(0, geometryProxy.size.width - 150)) } } diff --git a/Linphone/UI/Main/Fragments/PopupView.swift b/Linphone/UI/Main/Fragments/PopupView.swift index 784ed58ef..c80c57c62 100644 --- a/Linphone/UI/Main/Fragments/PopupView.swift +++ b/Linphone/UI/Main/Fragments/PopupView.swift @@ -20,7 +20,7 @@ import SwiftUI import Photos -struct PopupView: View { +struct PopupView: View { var permissionManager = PermissionManager.shared @@ -28,6 +28,8 @@ struct PopupView: View { var title: Text var content: Text? + private let additionalContent: AdditionalContent + var titleFirstButton: Text? var actionFirstButton: () -> Void @@ -37,6 +39,30 @@ struct PopupView: View { var titleThirdButton: Text? var actionThirdButton: () -> Void + init( + isShowPopup: Binding, + title: Text, + content: Text? = nil, + @ViewBuilder additionalContent: () -> AdditionalContent = { EmptyView() }, + titleFirstButton: Text? = nil, + actionFirstButton: @escaping () -> Void = {}, + titleSecondButton: Text? = nil, + actionSecondButton: @escaping () -> Void = {}, + titleThirdButton: Text? = nil, + actionThirdButton: @escaping () -> Void = {} + ) { + self._isShowPopup = isShowPopup + self.title = title + self.content = content + self.additionalContent = additionalContent() + self.titleFirstButton = titleFirstButton + self.actionFirstButton = actionFirstButton + self.titleSecondButton = titleSecondButton + self.actionSecondButton = actionSecondButton + self.titleThirdButton = titleThirdButton + self.actionThirdButton = actionThirdButton + } + var body: some View { GeometryReader { geometry in VStack(alignment: .leading) { @@ -52,6 +78,8 @@ struct PopupView: View { .padding(.bottom, 20) } + additionalContent + HStack { if titleFirstButton != nil { Button(action: { @@ -116,16 +144,3 @@ struct PopupView: View { } } } - -#Preview { - PopupView(isShowPopup: .constant(true), - title: Text("Title"), - content: Text("Content"), - titleFirstButton: Text("Accept all"), - actionFirstButton: {}, - titleSecondButton: Text("dialog_confirm"), - actionSecondButton: {}, - titleThirdButton: Text("dialog_cancel"), - actionThirdButton: {}) - .background(.black.opacity(0.65)) -} diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index 793da071d..a84ac2804 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -36,6 +36,9 @@ class SharedMainViewModel: ObservableObject { @Published var displayProfileMode = false @Published var defaultAvatar: URL? @Published var indexView: Int = 0 + @Published var increaseTrustLevelPopupAccepted = false + @Published var increaseTrustLevelPopupDeviceName = "" + @Published var increaseTrustLevelPopupDeviceAddress: Address? @Published var displayedFriend: ContactAvatarModel? @Published var displayedCall: HistoryModel? @@ -64,6 +67,7 @@ class SharedMainViewModel: ObservableObject { let displayProfileModeKey = "display_profile_mode" let defaultAvatarKey = "default_avatar" let indexViewKey = "index_view" + let increaseTrustLevelKey = "increase_trust_level" var maxWidth = 600.0 @@ -101,6 +105,12 @@ class SharedMainViewModel: ObservableObject { defaultAvatar = defaultAvatarTmp } } + + if preferences.object(forKey: increaseTrustLevelKey) == nil { + preferences.set(increaseTrustLevelPopupAccepted, forKey: increaseTrustLevelKey) + } else { + increaseTrustLevelPopupAccepted = preferences.bool(forKey: increaseTrustLevelKey) + } updateMissedCallsCount() updateDisableVideoCall() @@ -146,6 +156,14 @@ class SharedMainViewModel: ObservableObject { preferences.set(defaultAvatar, forKey: defaultAvatarKey) } + + func changeIncreaseTrustLevelPopupAccepted() { + let preferences = UserDefaults.standard + + increaseTrustLevelPopupAccepted = true + preferences.set(increaseTrustLevelPopupAccepted, forKey: increaseTrustLevelKey) + } + func changeIndexView(indexViewInt: Int) { let preferences = UserDefaults.standard diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index f59a49f66..3b8ee53a7 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ D7AEB9762F39E2A400298546 /* ConversationMediaListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9752F39E2A300298546 /* ConversationMediaListViewModel.swift */; }; D7AEB9782F39E2C300298546 /* ConversationDocumentsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9772F39E2C100298546 /* ConversationDocumentsListViewModel.swift */; }; D7AEB97A2F39E83600298546 /* FileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9792F39E83500298546 /* FileModel.swift */; }; + D7AEB9A02F3E219600298546 /* ContactDeviceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB99F2F3E218A00298546 /* ContactDeviceModel.swift */; }; D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; }; D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5678D2B28888F00DE63EB /* CallView.swift */; }; D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */; }; @@ -433,6 +434,7 @@ D7AEB9752F39E2A300298546 /* ConversationMediaListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMediaListViewModel.swift; sourceTree = ""; }; D7AEB9772F39E2C100298546 /* ConversationDocumentsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDocumentsListViewModel.swift; sourceTree = ""; }; D7AEB9792F39E83500298546 /* FileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileModel.swift; sourceTree = ""; }; + D7AEB99F2F3E218A00298546 /* ContactDeviceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDeviceModel.swift; sourceTree = ""; }; D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = ""; }; D7B5678D2B28888F00DE63EB /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = ""; }; @@ -822,6 +824,7 @@ D726E4372B1643FF0083C415 /* Model */ = { isa = PBXGroup; children = ( + D7AEB99F2F3E218A00298546 /* ContactDeviceModel.swift */, D726E4382B16440C0083C415 /* ContactAvatarModel.swift */, ); path = Model; @@ -1601,6 +1604,7 @@ D74DA0122C047F0700A8561D /* HistoryModel.swift in Sources */, 66D382052CEB7E0A0063E1C5 /* ShortcutModel.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, + D7AEB9A02F3E219600298546 /* ContactDeviceModel.swift in Sources */, D703F7082DC8C605005B8F75 /* FilePicker.swift in Sources */, C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */, D714DE622C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift in Sources */,