diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index d64aaf5db..b5bf29ff9 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -151,6 +151,8 @@ D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */; }; D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF32AFA66F900D938CB /* EditContactController.swift */; }; D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */; }; + D7C500402D27F16C00DD53EC /* AccountSettingsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C5003F2D27F16900DD53EC /* AccountSettingsFragment.swift */; }; + D7C500422D2BE98100DD53EC /* AccountSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C500412D2BE96E00DD53EC /* AccountSettingsViewModel.swift */; }; D7CEE0352B7A210300FD79B7 /* ConversationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */; }; D7CEE0382B7A214F00FD79B7 /* ConversationsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */; }; D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */; }; @@ -347,6 +349,8 @@ D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; D7C48DF32AFA66F900D938CB /* EditContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactController.swift; sourceTree = ""; }; D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerActionsFragment.swift; sourceTree = ""; }; + D7C5003F2D27F16900DD53EC /* AccountSettingsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsFragment.swift; sourceTree = ""; }; + D7C500412D2BE96E00DD53EC /* AccountSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewModel.swift; sourceTree = ""; }; D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsView.swift; sourceTree = ""; }; D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListViewModel.swift; sourceTree = ""; }; D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsFragment.swift; sourceTree = ""; }; @@ -927,6 +931,7 @@ D7DC096B2CFA192F00A6D47C /* Fragments */ = { isa = PBXGroup; children = ( + D7C5003F2D27F16900DD53EC /* AccountSettingsFragment.swift */, D7DC096E2CFA1D7400A6D47C /* AccountProfileFragment.swift */, ); path = Fragments; @@ -942,6 +947,7 @@ D7DC096D2CFA194600A6D47C /* ViewModel */ = { isa = PBXGroup; children = ( + D7C500412D2BE96E00DD53EC /* AccountSettingsViewModel.swift */, D7DC09702CFDBF8300A6D47C /* AccountProfileViewModel.swift */, ); path = ViewModel; @@ -1189,6 +1195,7 @@ C6A5A9452C10B6270070FEA4 /* OIDAuthStateExtension.swift in Sources */, D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */, + D7C500422D2BE98100DD53EC /* AccountSettingsViewModel.swift in Sources */, D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */, C67586B02C09F247002E77BF /* URIHandler.swift in Sources */, C62817282C1B389700DBA646 /* SideMenuAccountRow.swift in Sources */, @@ -1214,6 +1221,7 @@ D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */, 6646A7A32BB2E224006B842A /* ScheduleMeetingFragment.swift in Sources */, + D7C500402D27F16C00DD53EC /* AccountSettingsFragment.swift in Sources */, D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */, D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */, diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 4a0ebeeda..19838c969 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -150,6 +150,421 @@ }, "9" : { + }, + "A subject and at least one participant is required to create a meeting" : { + + }, + "Accept all" : { + + }, + "Account successfully logged out" : { + + }, + "account_settings_audio_video_conference_factory_uri_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio/video conference factory URI" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URI de l'usine à réunions" + } + } + } + }, + "account_settings_avpf_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AVPF" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "AVPF" + } + } + } + }, + "account_settings_bundle_mode_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bundle mode" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mode \"bundle\"" + } + } + } + }, + "account_settings_ccmp_server_url_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "CCMP server URL" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL du serveur CCMP" + } + } + } + }, + "account_settings_conference_factory_uri_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conference factory URI" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URI de l'usine à conversations" + } + } + } + }, + "account_settings_cpim_in_basic_conversations_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Use CPIM in \"basic\" conversations" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utiliser CPIM dans les conversations \"basiques\"" + } + } + } + }, + "account_settings_enable_ice_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable ICE" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer ICE" + } + } + } + }, + "account_settings_enable_turn_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable TURN" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer TURN" + } + } + } + }, + "account_settings_expire_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expire (in seconds)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expiration (en secondes)" + } + } + } + }, + "account_settings_im_encryption_mandatory_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "IM encryption mandatory" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiffrement obligatoire des conversations" + } + } + } + }, + "account_settings_lime_server_url_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "E2E encryption keys server URL" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL du serveur d'échange de clés de chiffrement" + } + } + } + }, + "account_settings_mwi_uri_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "MWI server URI (Message Waiting Indicator)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URI du serveur MWI (Message Waiting Indicator)" + } + } + } + }, + "account_settings_nat_policy_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "NAT policy settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres de politique NAT" + } + } + } + }, + "account_settings_outbound_proxy_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Outbound proxy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serveur mandataire sortant" + } + } + } + }, + "account_settings_push_notification_not_available_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Push notifications aren't available!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les notifications poussées ne sont pas disponibles !" + } + } + } + }, + "account_settings_push_notification_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allow push notifications" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autoriser les notifications poussées" + } + } + } + }, + "account_settings_sip_proxy_url_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SIP proxy server URL" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL du serveur mandataire" + } + } + } + }, + "account_settings_stun_server_url_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "STUN/TURN server URL" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL du serveur STUN/TURN" + } + } + } + }, + "account_settings_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres de compte" + } + } + } + }, + "account_settings_turn_password_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TURN password" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mot de passe TURN" + } + } + } + }, + "account_settings_turn_username_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TURN username" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilisateur TURN" + } + } + } + }, + "account_settings_update_password_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update password" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour le mot de passe" + } + } + } + }, + "account_settings_voicemail_uri_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voicemail URI" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URI du serveur de messagerie vocale" + } + } + } + }, + "Active" : { + + }, + "Administrateur" : { + + }, + "All calls will be removed from the history." : { + + }, + "All contacts" : { + + }, + "All modifications will be canceled." : { + }, "assistant_account_create" : { "localizations" : { @@ -5266,6 +5681,32 @@ } } }, + "Send cancellation notifications" : { + + }, + "Send Logs" : { + + }, + "Send notification to participants ?" : { + + }, + "settings_advanced_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advanced settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Paramètres avancés" + } + } + } + }, "settings_title" : { "extractionState" : "manual", "localizations" : { diff --git a/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift b/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift index 32b0d967d..55905fb1f 100644 --- a/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift +++ b/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift @@ -45,123 +45,178 @@ struct AccountProfileFragment: View { private let avatarSize = 100.0 var body: some View { - ZStack { - VStack(spacing: 1) { - Rectangle() - .foregroundColor(Color.orangeMain500) - .edgesIgnoringSafeArea(.top) - .frame(height: 0) - - HStack { - Image("caret-left") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - .padding(.top, 4) - .padding(.leading, -10) - .onTapGesture { - accountProfileViewModel.saveChangesWhenLeaving() - withAnimation { - if isShowAccountProfileFragment { - isShowAccountProfileFragment = false + NavigationView { + ZStack { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + .padding(.leading, -10) + .onTapGesture { + accountProfileViewModel.saveChangesWhenLeaving() + withAnimation { + if isShowAccountProfileFragment { + isShowAccountProfileFragment = false + } } } - } + + Text("manage_account_title") + .default_text_style_orange_800(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) - Text("manage_account_title") - .default_text_style_orange_800(styleSize: 16) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 4) - .lineLimit(1) - - Spacer() - } - .frame(maxWidth: .infinity) - .frame(height: 50) - .padding(.horizontal) - .padding(.bottom, 4) - .background(.white) - - ScrollView { - VStack(spacing: 0) { - if accountProfileViewModel.accountModelIndex != nil && CoreContext.shared.accounts.count > accountProfileViewModel.accountModelIndex! { - let accountModel = CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!] - VStack(spacing: 0) { - if #unavailable(iOS 16.0) { - Rectangle() - .foregroundColor(Color.gray100) - .frame(height: 7) - } - + ScrollView { + VStack(spacing: 0) { + if accountProfileViewModel.accountModelIndex != nil && CoreContext.shared.accounts.count > accountProfileViewModel.accountModelIndex! { + let accountModel = CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!] VStack(spacing: 0) { - if accountModel.avatarModel != nil - && accountModel.photoAvatarModel != nil - && !accountModel.photoAvatarModel!.isEmpty - && selectedImage == nil && !removedImage { - - AsyncImage(url: CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!].imagePathAvatar) { image in - switch image { - case .empty: - ProgressView() - .frame(width: avatarSize, height: avatarSize) - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) - case .failure: - Image(uiImage: contactsManager.textToImage( - firstName: accountModel.avatarModel?.name ?? "", - lastName: "")) - .resizable() - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) - @unknown default: - EmptyView() - } - } - } else if selectedImage == nil { - Image(uiImage: contactsManager.textToImage( - firstName: accountModel.avatarModel?.name ?? "", - lastName: "")) - .resizable() - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) - } else { - Image(uiImage: selectedImage!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) + if #unavailable(iOS 16.0) { + Rectangle() + .foregroundColor(Color.gray100) + .frame(height: 7) } - if accountModel.avatarModel != nil - && accountModel.photoAvatarModel != nil - && !accountModel.photoAvatarModel!.isEmpty - && (accountModel.photoAvatarModel!.suffix(11) != "default.png" || selectedImage != nil) - && !removedImage { - HStack { - Spacer() + VStack(spacing: 0) { + if accountModel.avatarModel != nil + && accountModel.photoAvatarModel != nil + && !accountModel.photoAvatarModel!.isEmpty + && selectedImage == nil && !removedImage { + AsyncImage(url: CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!].imagePathAvatar) { image in + switch image { + case .empty: + ProgressView() + .frame(width: avatarSize, height: avatarSize) + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + case .failure: + Image(uiImage: contactsManager.textToImage( + firstName: accountModel.avatarModel?.name ?? "", + lastName: "")) + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + @unknown default: + EmptyView() + } + } + } else if selectedImage == nil { + Image(uiImage: contactsManager.textToImage( + firstName: accountModel.avatarModel?.name ?? "", + lastName: "")) + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + } else { + Image(uiImage: selectedImage!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + } + + if accountModel.avatarModel != nil + && accountModel.photoAvatarModel != nil + && !accountModel.photoAvatarModel!.isEmpty + && (accountModel.photoAvatarModel!.suffix(11) != "default.png" || selectedImage != nil) + && !removedImage { + HStack { + Spacer() + + Button(action: { + showPhotoPicker = true + }, label: { + HStack { + Image("pencil-simple") + .resizable() + .frame(width: 20, height: 20) + + Text("manage_account_edit_picture") + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + } + }) + .padding(.top, 10) + .padding(.trailing, 10) + .sheet(isPresented: $showPhotoPicker) { + PhotoPicker(filter: .images, limit: 1) { results in + PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in + if let error = errorOrNil { + print(error) + } + if let images = imagesOrNil { + if let first = images.first { + selectedImage = first + removedImage = false + saveImage() + } + } + } + } + .edgesIgnoringSafeArea(.all) + } + + Button(action: { + removedImage = true + selectedImage = nil + saveImage() + }, label: { + HStack { + Image("trash-simple") + .resizable() + .frame(width: 20, height: 20) + + Text("manage_account_remove_picture") + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + } + }) + .padding(.top, 10) + + Spacer() + } + } else { Button(action: { showPhotoPicker = true }, label: { HStack { - Image("pencil-simple") + Image("camera") .resizable() .frame(width: 20, height: 20) - Text("manage_account_edit_picture") + Text("manage_account_add_picture") .foregroundStyle(Color.grayMain2c700) .multilineTextAlignment(.center) .default_text_style(styleSize: 14) } }) .padding(.top, 10) - .padding(.trailing, 10) .sheet(isPresented: $showPhotoPicker) { PhotoPicker(filter: .images, limit: 1) { results in PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in @@ -172,6 +227,7 @@ struct AccountProfileFragment: View { if let first = images.first { selectedImage = first removedImage = false + showPhotoPicker = false saveImage() } } @@ -179,184 +235,79 @@ struct AccountProfileFragment: View { } .edgesIgnoringSafeArea(.all) } - - Button(action: { - removedImage = true - selectedImage = nil - saveImage() - }, label: { - HStack { - Image("trash-simple") - .resizable() - .frame(width: 20, height: 20) - - Text("manage_account_remove_picture") - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - } - }) - .padding(.top, 10) - - Spacer() - } - } else { - Button(action: { - showPhotoPicker = true - }, label: { - HStack { - Image("camera") - .resizable() - .frame(width: 20, height: 20) - - Text("manage_account_add_picture") - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - } - }) - .padding(.top, 10) - .sheet(isPresented: $showPhotoPicker) { - PhotoPicker(filter: .images, limit: 1) { results in - PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in - if let error = errorOrNil { - print(error) - } - if let images = imagesOrNil { - if let first = images.first { - selectedImage = first - removedImage = false - showPhotoPicker = false - saveImage() - } - } - } - } - .edgesIgnoringSafeArea(.all) } } - } - .frame(minHeight: 150) - .frame(maxWidth: .infinity) - .padding(.top, 10) - .padding(.bottom, 2) - .background(Color.gray100) - - HStack(alignment: .center) { - Text("manage_account_details_title") - .default_text_style_800(styleSize: 18) - .frame(maxWidth: .infinity, alignment: .leading) + .frame(minHeight: 150) + .frame(maxWidth: .infinity) + .padding(.top, 10) + .padding(.bottom, 2) + .background(Color.gray100) - Spacer() - - Image(detailIsOpen ? "caret-up" : "caret-down") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - } - .padding(.top, 30) - .padding(.bottom, 10) - .padding(.horizontal, 20) - .background(Color.gray100) - .onTapGesture { - withAnimation { - detailIsOpen.toggle() + HStack(alignment: .center) { + Text("manage_account_details_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image(detailIsOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) } - } - - if detailIsOpen { - if accountModel.avatarModel != nil { - VStack(spacing: 0) { - VStack(spacing: 30) { - HStack { - Text(String(localized: "sip_address") + ":") - .default_text_style_700(styleSize: 15) - - Text(accountModel.avatarModel!.address) - .foregroundStyle(Color.grayMain2c700) - .default_text_style(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) - - Button(action: { - UIPasteboard.general.setValue( - accountModel.avatarModel!.address, - forPasteboardType: UTType.plainText.identifier - ) - - ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard" - ToastViewModel.shared.displayToast.toggle() - }, label: { - Image("copy") - .resizable() - .frame(width: 20, height: 20) - }) - } - - VStack(alignment: .leading) { - Text("sip_address_display_name") - .default_text_style_700(styleSize: 15) - .padding(.bottom, -5) - - TextField(accountModel.displayNameAvatar, text: Binding( - get: { accountModel.displayNameAvatar }, - set: { newValue in - accountModel.displayNameAvatar = newValue - } - )) - .default_text_style(styleSize: 15) - .frame(height: 25) - .padding(.horizontal, 20) - .padding(.vertical, 15) - .background(.white) - .cornerRadius(60) - .overlay( - RoundedRectangle(cornerRadius: 60) - .inset(by: 0.5) - .stroke(isDisplayNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) - ) - .focused($isDisplayNameFocused) - } - - VStack(alignment: .leading) { + .padding(.top, 30) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .background(Color.gray100) + .onTapGesture { + withAnimation { + detailIsOpen.toggle() + } + } + + if detailIsOpen { + if accountModel.avatarModel != nil { + VStack(spacing: 0) { + VStack(spacing: 30) { HStack { - Text("manage_account_international_prefix") + Text(String(localized: "sip_address") + ":") .default_text_style_700(styleSize: 15) - .padding(.bottom, -5) + + Text(accountModel.avatarModel!.address) + .foregroundStyle(Color.grayMain2c700) + .default_text_style(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) Button(action: { - isShowPopup = true + UIPasteboard.general.setValue( + accountModel.avatarModel!.address, + forPasteboardType: UTType.plainText.identifier + ) + + ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() }, label: { - Image("question") - .renderingMode(.template) + Image("copy") .resizable() - .foregroundStyle(Color.grayMain2c600) .frame(width: 20, height: 20) }) - .padding(.bottom, -5) } - Menu { - Picker("", selection: $accountProfileViewModel.dialPlanValueSelected) { - ForEach(registerViewModel.dialPlansLabelList, id: \.self) { dialPlan in - Text(dialPlan).tag(dialPlan) + + VStack(alignment: .leading) { + Text("sip_address_display_name") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField(accountModel.displayNameAvatar, text: Binding( + get: { accountModel.displayNameAvatar }, + set: { newValue in + accountModel.displayNameAvatar = newValue } - } - .onChange(of: accountProfileViewModel.dialPlanValueSelected) { newValue in - accountProfileViewModel.updateDialPlan(newDialPlan: newValue) - } - } label: { - HStack { - Text(accountProfileViewModel.dialPlanValueSelected) - .default_text_style(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - - Image("caret-down") - .resizable() - .frame(width: 20, height: 20) - } + )) + .default_text_style(styleSize: 15) .frame(height: 25) .padding(.horizontal, 20) .padding(.vertical, 15) @@ -365,284 +316,345 @@ struct AccountProfileFragment: View { .overlay( RoundedRectangle(cornerRadius: 60) .inset(by: 0.5) - .stroke(Color.gray200, lineWidth: 1) + .stroke(isDisplayNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) ) + .focused($isDisplayNameFocused) + } + + VStack(alignment: .leading) { + HStack { + Text("manage_account_international_prefix") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + .lineLimit(1) + + Button(action: { + isShowPopup = true + }, label: { + Image("question") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20) + }) + .padding(.bottom, -5) + } + Menu { + Picker("", selection: $accountProfileViewModel.dialPlanValueSelected) { + ForEach(registerViewModel.dialPlansLabelList, id: \.self) { dialPlan in + Text(dialPlan).tag(dialPlan) + } + } + .onChange(of: accountProfileViewModel.dialPlanValueSelected) { newValue in + accountProfileViewModel.updateDialPlan(newDialPlan: newValue) + } + } label: { + HStack { + Text(accountProfileViewModel.dialPlanValueSelected) + .default_text_style(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + + Image("caret-down") + .resizable() + .frame(width: 20, height: 20) + } + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.gray200, lineWidth: 1) + ) + } } } + .padding(.vertical, 30) + .padding(.horizontal, 20) } - .padding(.vertical, 30) - .padding(.horizontal, 20) + .background(.white) + .cornerRadius(15) + .padding(.horizontal) + .zIndex(-1) + .transition(.move(edge: .top)) } - .background(.white) - .cornerRadius(15) - .padding(.horizontal) - .zIndex(-1) - .transition(.move(edge: .top)) } - } - - VStack(spacing: 0) { - VStack(spacing: 15) { - HStack(spacing: 20) { - Toggle("", isOn: Binding( - get: { accountModel.isRegistrered }, - set: { _ in - accountProfileViewModel.toggleRegister() - } - )) - .labelsHidden() - - Text(accountModel.humanReadableRegistrationState) - .default_text_style_700(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, -5) - .lineLimit(1) - } - - Text(accountModel.summary) - .default_text_style(styleSize: 15) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, -5) - } - .padding(.vertical, 30) - .padding(.horizontal, 20) - } - .background(.white) - .cornerRadius(15) - .padding(.all) - .background(Color.gray100) - - HStack(alignment: .center) { - Text("manage_account_devices_title") - .default_text_style_800(styleSize: 18) - .frame(maxWidth: .infinity, alignment: .leading) - Spacer() - - Image(deviceIsOpen ? "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, 20) - .background(Color.gray100) - .onTapGesture { - withAnimation { - deviceIsOpen.toggle() - } - } - - if deviceIsOpen { VStack(spacing: 0) { VStack(spacing: 15) { - ForEach(accountModel.devices.indices, id: \.self) { index in - VStack { - HStack { - Image(accountModel.devices[index].isMobileDevice ? "device-mobile-camera" : "desktop") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) + HStack(spacing: 20) { + Toggle("", isOn: Binding( + get: { accountModel.isRegistrered }, + set: { _ in + accountProfileViewModel.toggleRegister() + } + )) + .labelsHidden() + + Text(accountModel.humanReadableRegistrationState) + .default_text_style_700(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, -5) + .lineLimit(1) + } + + Text(accountModel.summary) + .default_text_style(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, -5) + } + .padding(.vertical, 30) + .padding(.horizontal, 20) + } + .background(.white) + .cornerRadius(15) + .padding(.all) + .background(Color.gray100) + + HStack(alignment: .center) { + Text("manage_account_devices_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image(deviceIsOpen ? "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, 20) + .background(Color.gray100) + .onTapGesture { + withAnimation { + deviceIsOpen.toggle() + } + } + + if deviceIsOpen { + VStack(spacing: 0) { + VStack(spacing: 15) { + ForEach(accountModel.devices.indices, id: \.self) { index in + VStack { + HStack { + Image(accountModel.devices[index].isMobileDevice ? "device-mobile-camera" : "desktop") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + + Text(accountModel.devices[index].deviceName) + .default_text_style_700(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + Button(action: { + deviceIsOpen = false + accountModel.removeDevice(deviceIndex: index) + deviceIsOpen = true + }, label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 20, height: 20) + + Text("manage_account_device_remove") + .default_text_style_orange_500(styleSize: 14) + .frame(height: 35) + } + + }) + .padding(.horizontal, 10) + .background(Color.orangeMain100) + .cornerRadius(60) + } + .padding(.bottom, 10) - Text(accountModel.devices[index].deviceName) + Text("manage_account_device_last_connection") .default_text_style_700(styleSize: 15) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) - Button(action: { - deviceIsOpen = false - accountModel.removeDevice(deviceIndex: index) - deviceIsOpen = true - }, label: { - HStack { - Image("trash-simple") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 20, height: 20) - - Text("manage_account_device_remove") - .default_text_style_orange_500(styleSize: 14) - .frame(height: 35) - } + HStack { + Image("calendar-blank") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) - }) - .padding(.horizontal, 10) - .background(Color.orangeMain100) - .cornerRadius(60) + Text(accountModel.devices[index].lastDate) + .default_text_style(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + Image("clock") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + + Text(accountModel.devices[index].lastTime) + .default_text_style(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + } } - .padding(.bottom, 10) + .padding(.all, 20) + .background(Color.gray100) + .cornerRadius(15) - Text("manage_account_device_last_connection") - .default_text_style_700(styleSize: 15) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - - HStack { - Image("calendar-blank") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) - - Text(accountModel.devices[index].lastDate) - .default_text_style(styleSize: 15) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - - Image("clock") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) - - Text(accountModel.devices[index].lastTime) - .default_text_style(styleSize: 15) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(.all, 20) + .frame(maxWidth: .infinity) + .overlay( + VStack { + if accountModel.devices.indices.isEmpty { + Text("manage_account_no_device") + .default_text_style_500(styleSize: 16) } } - .padding(.all, 20) - .background(Color.gray100) - .cornerRadius(15) - - } + .padding(.all) + ) } - .padding(.all, 20) - .frame(maxWidth: .infinity) - .overlay( - VStack { - if accountModel.devices.indices.isEmpty { - Text("manage_account_no_device") - .default_text_style_500(styleSize: 16) - } + .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: 18) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.top, 20) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .background(Color.gray100) + + VStack(spacing: 0) { + VStack(spacing: 18) { + if accountProfileViewModel.accountModelIndex != nil && CoreContext.shared.accounts.count > accountProfileViewModel.accountModelIndex! { + NavigationLink( + destination: + AccountSettingsFragment( + accountModel: CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!] + ), + label: { + HStack { + Image("gear") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c700) + .frame(width: 25, height: 25) + + Text("manage_account_settings") + .foregroundStyle(Color.grayMain2c700) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + ) } - .padding(.all) - ) + + Divider() + + Button(action: { + isShowLogoutPopup = true + }, label: { + HStack { + Image("sign-out") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 25, height: 25) + + Text("manage_account_delete") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + } + }) + } + .padding(.vertical, 20) + .padding(.horizontal, 20) } .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: 18) - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: sharedMainViewModel.maxWidth) + .onAppear { + accountModel.requestDevicesList() } - .padding(.top, 20) - .padding(.bottom, 10) - .padding(.horizontal, 20) - .background(Color.gray100) - - VStack(spacing: 0) { - VStack(spacing: 18) { - Button(action: { - }, label: { - HStack { - Image("gear") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c700) - .frame(width: 25, height: 25) - - Text("manage_account_settings") - .foregroundStyle(Color.grayMain2c700) - .default_text_style(styleSize: 16) - .frame(maxWidth: .infinity, alignment: .leading) - } - }) - - Divider() - - Button(action: { - isShowLogoutPopup = true - }, label: { - HStack { - Image("sign-out") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.redDanger500) - .frame(width: 25, height: 25) - - Text("manage_account_delete") - .foregroundStyle(Color.redDanger500) - .default_text_style(styleSize: 16) - .frame(maxWidth: .infinity, alignment: .leading) - } - }) - } - .padding(.vertical, 20) - .padding(.horizontal, 20) - } - .background(.white) - .cornerRadius(15) - .padding(.horizontal) - } - .frame(maxWidth: sharedMainViewModel.maxWidth) - .onAppear { - accountModel.requestDevicesList() } } + .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity) + .background(Color.gray100) } .background(Color.gray100) - } - .background(Color.gray100) - - if self.isShowPopup { - PopupView(isShowPopup: $isShowPopup, - title: Text("manage_account_international_prefix"), - content: Text("manage_account_dialog_international_prefix_help_message"), - titleFirstButton: nil, - actionFirstButton: {}, - titleSecondButton: Text("Ok"), - actionSecondButton: { - self.isShowPopup.toggle() - } - ) - .background(.black.opacity(0.65)) - .onTapGesture { - self.isShowPopup.toggle() - } - } - - if self.isShowLogoutPopup { - let localizedString = NSLocalizedString("manage_account_dialog_remove_account_message", comment: "") - let components = localizedString.components(separatedBy: " ") - let textPart = components.dropLast().joined(separator: " ") - - let contentPopup1 = Text(textPart + " ") - let contentPopup2 = Text("[https://sip.linphone.org](https://sip.linphone.org)").underline() - - PopupView( - isShowPopup: $isShowLogoutPopup, - title: Text("manage_account_dialog_remove_account_title"), - content: contentPopup1 + contentPopup2, - titleFirstButton: Text("Cancel"), - actionFirstButton: { - self.isShowLogoutPopup.toggle() - }, - titleSecondButton: Text("manage_account_delete"), - actionSecondButton: { - if accountProfileViewModel.accountModelIndex != nil { - CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!].logout() - } + if self.isShowPopup { + PopupView(isShowPopup: $isShowPopup, + title: Text("manage_account_international_prefix"), + content: Text("manage_account_dialog_international_prefix_help_message"), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("Ok"), + actionSecondButton: { + self.isShowPopup.toggle() + } + ) + .background(.black.opacity(0.65)) + .onTapGesture { + self.isShowPopup.toggle() + } + } + + if self.isShowLogoutPopup { + let localizedString = NSLocalizedString("manage_account_dialog_remove_account_message", comment: "") + + let components = localizedString.components(separatedBy: " ") + let textPart = components.dropLast().joined(separator: " ") + + let contentPopup1 = Text(textPart + " ") + let contentPopup2 = Text("[https://sip.linphone.org](https://sip.linphone.org)").underline() + + PopupView( + isShowPopup: $isShowLogoutPopup, + title: Text("manage_account_dialog_remove_account_title"), + content: contentPopup1 + contentPopup2, + titleFirstButton: Text("Cancel"), + actionFirstButton: { + self.isShowLogoutPopup.toggle() + }, + titleSecondButton: Text("manage_account_delete"), + actionSecondButton: { + if accountProfileViewModel.accountModelIndex != nil { + CoreContext.shared.accounts[accountProfileViewModel.accountModelIndex!].logout() + } + } + ) + .background(.black.opacity(0.65)) + .onTapGesture { + self.isShowLogoutPopup.toggle() } - ) - .background(.black.opacity(0.65)) - .onTapGesture { - self.isShowLogoutPopup.toggle() } } + .navigationTitle("") + .navigationBarHidden(true) } + .navigationViewStyle(StackNavigationViewStyle()) } func saveImage() { diff --git a/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift b/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift new file mode 100644 index 000000000..f2b28a1d7 --- /dev/null +++ b/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift @@ -0,0 +1,598 @@ +/* + * Copyright (c) 2010-2023 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 + +// swiftlint:disable type_body_length +struct AccountSettingsFragment: View { + + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + + @StateObject private var accountSettingsViewModel: AccountSettingsViewModel + + @Environment(\.dismiss) var dismiss + + @State var natPolicySettingsIsOpen: Bool = false + @State var advancedSettingsIsOpen: Bool = false + @State var isSecured: Bool = true + + @FocusState var isVoicemailUriFocused: Bool + @FocusState var isMwiUriFocused: Bool + @FocusState var isStunServerUriFocused: Bool + @FocusState var isTurnUsernameFocused: Bool + @FocusState var isTurnPasswordFocused: Bool + @FocusState var isSipProxyUrlFocused: Bool + @FocusState var isSettingsExpireFocused: Bool + @FocusState var isConferenceFactoryUriFocused: Bool + @FocusState var isAudioVideoConferenceFactoryUriFocused: Bool + @FocusState var isCcmpServerUrlFocused: Bool + @FocusState var isLimeServerUrlFocused: Bool + + init(accountModel: AccountModel) { + _accountSettingsViewModel = StateObject(wrappedValue: AccountSettingsViewModel(accountModel: accountModel)) + } + + var body: some View { + ZStack { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + .padding(.leading, -10) + .onTapGesture { + accountSettingsViewModel.saveChanges() + dismiss() + } + + Text("account_settings_title") + .default_text_style_orange_800(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + VStack(spacing: 0) { + VStack(spacing: 0) { + VStack(spacing: 30) { + Toggle("account_settings_push_notification_title", isOn: Binding( + get: { accountSettingsViewModel.pushNotification }, + set: { _ in + accountSettingsViewModel.pushNotification.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + Toggle("account_settings_im_encryption_mandatory_title", isOn: Binding( + get: { accountSettingsViewModel.imEncryptionMandatory }, + set: { _ in + accountSettingsViewModel.imEncryptionMandatory.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + VStack(alignment: .leading) { + Text("account_settings_voicemail_uri_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_voicemail_uri_title", text: Binding( + get: { accountSettingsViewModel.voicemailUri }, + set: { newValue in + accountSettingsViewModel.voicemailUri = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isVoicemailUriFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isVoicemailUriFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_mwi_uri_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_mwi_uri_title", text: Binding( + get: { accountSettingsViewModel.mwiUri }, + set: { newValue in + accountSettingsViewModel.mwiUri = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isMwiUriFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isMwiUriFocused) + } + } + .padding(.vertical, 30) + .padding(.horizontal, 20) + } + .background(.white) + .cornerRadius(15) + .padding(.horizontal) + .padding(.top, 10) + .background(Color.gray100) + + HStack(alignment: .center) { + Text("account_settings_nat_policy_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image(natPolicySettingsIsOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + .padding(.top, 30) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .background(Color.gray100) + .onTapGesture { + withAnimation { + natPolicySettingsIsOpen.toggle() + } + } + + if natPolicySettingsIsOpen { + if accountSettingsViewModel.accountModel.avatarModel != nil { + VStack(spacing: 0) { + VStack(spacing: 30) { + VStack(alignment: .leading) { + Text("account_settings_stun_server_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_stun_server_url_title", text: Binding( + get: { accountSettingsViewModel.stunServerUrl }, + set: { newValue in + accountSettingsViewModel.stunServerUrl = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isStunServerUriFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isStunServerUriFocused) + } + + Toggle("account_settings_enable_ice_title", isOn: Binding( + get: { accountSettingsViewModel.enableIce }, + set: { _ in + accountSettingsViewModel.enableIce.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + Toggle("account_settings_enable_turn_title", isOn: Binding( + get: { accountSettingsViewModel.enableTurn }, + set: { _ in + accountSettingsViewModel.enableTurn.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + VStack(alignment: .leading) { + Text("account_settings_turn_username_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_turn_username_title", text: Binding( + get: { accountSettingsViewModel.turnUsername }, + set: { newValue in + accountSettingsViewModel.turnUsername = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isTurnUsernameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isTurnUsernameFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_turn_password_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + ZStack(alignment: .trailing) { + Group { + if isSecured { + SecureField("account_settings_turn_password_title", text: Binding( + get: { accountSettingsViewModel.turnPassword }, + set: { newValue in + accountSettingsViewModel.turnPassword = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isTurnPasswordFocused) + } else { + TextField("account_settings_turn_password_title", text: Binding( + get: { accountSettingsViewModel.turnPassword }, + set: { newValue in + accountSettingsViewModel.turnPassword = newValue + } + )) + .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) + .frame(height: 25) + .focused($isTurnPasswordFocused) + } + } + + Button(action: { + isSecured.toggle() + }, label: { + Image(self.isSecured ? "eye-slash" : "eye") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 20, height: 20) + }) + } + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isTurnPasswordFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.bottom) + } + } + .padding(.vertical, 30) + .padding(.horizontal, 20) + } + .background(.white) + .cornerRadius(15) + .padding(.horizontal) + .zIndex(-1) + .transition(.move(edge: .top)) + } + } + + HStack(alignment: .center) { + Text("settings_advanced_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image(advancedSettingsIsOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + .padding(.top, 30) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .background(Color.gray100) + .onTapGesture { + withAnimation { + advancedSettingsIsOpen.toggle() + } + } + + if advancedSettingsIsOpen { + if accountSettingsViewModel.accountModel.avatarModel != nil { + VStack(spacing: 0) { + VStack(spacing: 30) { + VStack(alignment: .leading) { + Text("Transport") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + Menu { + Button("TLS") { accountSettingsViewModel.transport = "TLS" } + Button("TCP") { accountSettingsViewModel.transport = "TCP" } + Button("UDP") { accountSettingsViewModel.transport = "UDP" } + } label: { + Text(accountSettingsViewModel.transport) + .default_text_style(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + Image("caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 20, height: 20) + } + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.gray200, lineWidth: 1) + ) + } + + VStack(alignment: .leading) { + Text("account_settings_sip_proxy_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_sip_proxy_url_title", text: Binding( + get: { accountSettingsViewModel.sipProxyUrl }, + set: { newValue in + accountSettingsViewModel.sipProxyUrl = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSipProxyUrlFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isSipProxyUrlFocused) + } + + Toggle("account_settings_outbound_proxy_title", isOn: Binding( + get: { accountSettingsViewModel.outboundProxy }, + set: { _ in + //accountProfileViewModel.toggleRegister() + } + )) + .default_text_style_700(styleSize: 15) + + Toggle("account_settings_avpf_title", isOn: Binding( + get: { accountSettingsViewModel.avpf }, + set: { _ in + accountSettingsViewModel.avpf.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + Toggle("account_settings_bundle_mode_title", isOn: Binding( + get: { accountSettingsViewModel.bundleMode }, + set: { _ in + accountSettingsViewModel.bundleMode.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + Toggle("account_settings_cpim_in_basic_conversations_title", isOn: Binding( + get: { accountSettingsViewModel.cpimInBasicConversations }, + set: { _ in + accountSettingsViewModel.cpimInBasicConversations.toggle() + } + )) + .default_text_style_700(styleSize: 15) + + VStack(alignment: .leading) { + Text("account_settings_expire_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_expire_title", text: Binding( + get: { accountSettingsViewModel.expire }, + set: { newValue in + accountSettingsViewModel.expire = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSettingsExpireFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isSettingsExpireFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_conference_factory_uri_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_conference_factory_uri_title", text: Binding( + get: { accountSettingsViewModel.conferenceFactoryUri }, + set: { newValue in + accountSettingsViewModel.conferenceFactoryUri = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isConferenceFactoryUriFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isConferenceFactoryUriFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_audio_video_conference_factory_uri_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_audio_video_conference_factory_uri_title", text: Binding( + get: { accountSettingsViewModel.audioVideoConferenceFactoryUri }, + set: { newValue in + accountSettingsViewModel.audioVideoConferenceFactoryUri = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isAudioVideoConferenceFactoryUriFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isAudioVideoConferenceFactoryUriFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_ccmp_server_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_ccmp_server_url_title", text: Binding( + get: { accountSettingsViewModel.ccmpServerUrl }, + set: { newValue in + accountSettingsViewModel.ccmpServerUrl = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isCcmpServerUrlFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isCcmpServerUrlFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_lime_server_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_lime_server_url_title", text: Binding( + get: { accountSettingsViewModel.limeServerUrl }, + set: { newValue in + accountSettingsViewModel.limeServerUrl = newValue + } + )) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isLimeServerUrlFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isLimeServerUrlFocused) + } + + /* + Button { + // TODO Update password + } label: { + Text("account_settings_update_password_title") + .default_text_style_700(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + } + */ + } + .padding(.vertical, 30) + .padding(.horizontal, 20) + } + .background(.white) + .cornerRadius(15) + .padding(.horizontal) + .zIndex(-2) + .transition(.move(edge: .top)) + } + } + } + .frame(maxWidth: sharedMainViewModel.maxWidth) + } + .frame(maxWidth: .infinity) + } + .background(Color.gray100) + } + .background(Color.gray100) + } + .navigationTitle("") + .navigationBarHidden(true) + } +} +// swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift b/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift new file mode 100644 index 000000000..92dfc499b --- /dev/null +++ b/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2010-2023 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 linphonesw + +class AccountSettingsViewModel: ObservableObject { + + static let TAG = "[AccountSettingsViewModel]" + + @Published var accountModel: AccountModel + + @Published var pushNotification: Bool + @Published var imEncryptionMandatory: Bool + @Published var voicemailUri: String + @Published var mwiUri: String + @Published var stunServerUrl: String + @Published var enableIce: Bool + @Published var enableTurn: Bool + @Published var turnUsername: String + @Published var turnPassword: String + @Published var transport: String + @Published var sipProxyUrl: String + @Published var outboundProxy: Bool + @Published var avpf: Bool + @Published var bundleMode: Bool + @Published var cpimInBasicConversations: Bool + @Published var expire: String + @Published var conferenceFactoryUri: String + @Published var audioVideoConferenceFactoryUri: String + @Published var ccmpServerUrl: String + @Published var limeServerUrl: String + + private var natPolicy: NatPolicy? + private var natPolicyAuthInfo: AuthInfo? + + init(accountModel: AccountModel) { + self.accountModel = accountModel + + self.pushNotification = accountModel.account.params?.pushNotificationAllowed ?? false + self.imEncryptionMandatory = accountModel.account.params?.instantMessagingEncryptionMandatory ?? false + self.voicemailUri = accountModel.account.params?.voicemailAddress?.asStringUriOnly() ?? "" + self.mwiUri = accountModel.account.params?.mwiServerAddress?.asStringUriOnly() ?? "" + + self.natPolicy = accountModel.account.params?.natPolicy + self.stunServerUrl = accountModel.account.params?.natPolicy?.stunServer ?? "" + self.enableIce = accountModel.account.params?.natPolicy?.iceEnabled ?? false + self.enableTurn = accountModel.account.params?.natPolicy?.turnEnabled ?? false + let turnUsernameTmp = accountModel.account.params?.natPolicy?.stunServerUsername ?? "" + self.turnUsername = turnUsernameTmp + self.turnPassword = "" + + let transportTmp = accountModel.account.params?.transport + if transportTmp == .Tls { + self.transport = "TLS" + } else if transportTmp == .Tcp { + self.transport = "TCP" + } else if transportTmp == .Udp { + self.transport = "UDP" + } else { + self.transport = "DTLS" + } + + self.sipProxyUrl = accountModel.account.params?.serverAddress?.asStringUriOnly() ?? "" + self.outboundProxy = accountModel.account.params?.outboundProxyEnabled ?? false + self.avpf = accountModel.account.avpfEnabled + self.bundleMode = accountModel.account.params?.rtpBundleEnabled ?? false + self.cpimInBasicConversations = accountModel.account.params?.cpimInBasicChatRoomEnabled ?? false + self.expire = accountModel.account.params?.expires.description ?? "" + self.conferenceFactoryUri = accountModel.account.params?.conferenceFactoryAddress?.asStringUriOnly() ?? "" + self.audioVideoConferenceFactoryUri = accountModel.account.params?.audioVideoConferenceFactoryAddress?.asStringUriOnly() ?? "" + self.ccmpServerUrl = accountModel.account.params?.ccmpServerUrl ?? "" + self.limeServerUrl = accountModel.account.params?.limeServerUrl ?? "" + + if !turnUsernameTmp.isEmpty { + CoreContext.shared.doOnCoreQueue { core in + let authInfo = core.findAuthInfo(realm: nil, username: turnUsernameTmp, sipDomain: nil) + if authInfo == nil { + Log.warn("\(AccountSettingsViewModel.TAG) TURN username not empty but unable to find matching auth info!") + } else { + self.natPolicyAuthInfo = authInfo! + DispatchQueue.main.async { + self.turnPassword = authInfo!.password ?? "" + } + } + + if accountModel.account.params?.natPolicy == nil { + self.natPolicy = try? core.createNatPolicy() + } + } + } + } + + func saveChanges() { + CoreContext.shared.doOnCoreQueue { core in + print("\(AccountSettingsViewModel.TAG) Saving changes...") + + if let newParams = self.accountModel.account.params?.clone() { + newParams.pushNotificationAllowed = self.pushNotification + newParams.remotePushNotificationAllowed = self.pushNotification + + newParams.instantMessagingEncryptionMandatory = self.imEncryptionMandatory + + if !self.sipProxyUrl.isEmpty { + if let serverAddress = core.interpretUrl(url: self.sipProxyUrl, applyInternationalPrefix: false) { + + var transportTmp: TransportType = .Tls + + if self.transport == "TLS" { + transportTmp = .Tls + } else if self.transport == "TCP" { + transportTmp = .Tcp + } else if self.transport == "UDP" { + transportTmp = .Udp + } else { + transportTmp = .Dtls + } + + try? serverAddress.setTransport(newValue: transportTmp) + try? newParams.setServeraddress(newValue: serverAddress) + } + } + newParams.outboundProxyEnabled = self.outboundProxy + + if let natPolicy = self.natPolicy { + print("\(AccountSettingsViewModel.TAG) Also applying changes to NAT policy") + natPolicy.stunServer = self.stunServerUrl + natPolicy.stunEnabled = !self.stunServerUrl.isEmpty + natPolicy.iceEnabled = self.enableIce + natPolicy.turnEnabled = self.enableTurn + let stunTurnUsername = self.turnUsername + natPolicy.stunServerUsername = stunTurnUsername + newParams.natPolicy = natPolicy + + if let natPolicyAuthInfo = self.natPolicyAuthInfo { + if stunTurnUsername.isEmpty { + print("\(AccountSettingsViewModel.TAG) NAT policy TURN username is now empty, removing existing auth info") + core.removeAuthInfo(info: natPolicyAuthInfo) + } else { + print("\(AccountSettingsViewModel.TAG) Found NAT policy auth info, updating it") + natPolicyAuthInfo.username = stunTurnUsername + natPolicyAuthInfo.password = self.turnPassword + } + } else if !stunTurnUsername.isEmpty { + print("\(AccountSettingsViewModel.TAG) No NAT policy auth info found, creating it with") + if let authInfo = try? Factory.Instance.createAuthInfo( + username: stunTurnUsername, + userid: nil, + passwd: self.turnPassword, + ha1: nil, + realm: nil, + domain: nil + ) { + core.addAuthInfo(info: authInfo) + } + } + } + + newParams.avpfMode = self.avpf ? .Enabled : .Disabled + + newParams.rtpBundleEnabled = self.bundleMode + + newParams.cpimInBasicChatRoomEnabled = self.cpimInBasicConversations + + if !self.mwiUri.isEmpty { + newParams.mwiServerAddress = core.interpretUrl(url: self.mwiUri, applyInternationalPrefix: false) + } else { + newParams.mwiServerAddress = nil + } + + if !self.voicemailUri.isEmpty { + newParams.voicemailAddress = core.interpretUrl(url: self.voicemailUri, applyInternationalPrefix: false) + } else { + newParams.voicemailAddress = nil + } + + let expireInt: Int = { + if !self.expire.isEmpty { + return Int(self.expire) ?? 31536000 + } + return 31536000 + }() + + newParams.expires = expireInt + + if !self.conferenceFactoryUri.isEmpty { + newParams.conferenceFactoryAddress = core.interpretUrl(url: self.conferenceFactoryUri, applyInternationalPrefix: false) + } else { + newParams.conferenceFactoryAddress = nil + } + + if !self.audioVideoConferenceFactoryUri.isEmpty { + newParams.audioVideoConferenceFactoryAddress = core.interpretUrl(url: self.audioVideoConferenceFactoryUri, applyInternationalPrefix: false) + } else { + newParams.audioVideoConferenceFactoryAddress = nil + } + + newParams.ccmpServerUrl = self.ccmpServerUrl + newParams.limeServerUrl = self.limeServerUrl + + self.accountModel.account.params = newParams + print("\(AccountSettingsViewModel.TAG) Changes have been saved") + } + } + } +}