diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 502174189..2eb923525 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -113,6 +113,8 @@ final class ContactsManager: ObservableObject { core.addFriendList(list: tempRemoteFriendList) } } + + self.refreshCardDavContacts() } let store = CNContactStore() @@ -229,6 +231,22 @@ final class ContactsManager: ObservableObject { } } + func imageFromBase64(_ base64String: String) -> UIImage? { + let cleanedString: String + if let range = base64String.range(of: "base64,") { + cleanedString = String(base64String[range.upperBound...]) + } else { + cleanedString = base64String + } + + guard let imageData = Data(base64Encoded: cleanedString, options: .ignoreUnknownCharacters) else { + print("Error: failed to decode Base64 string") + return nil + } + + return UIImage(data: imageData) + } + func saveImage(image: UIImage, name: String, prefix: String, contact: Contact, linphoneFriend: String, existingFriend: Friend?, completion: @escaping () -> Void) { guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else { return @@ -242,7 +260,9 @@ final class ContactsManager: ObservableObject { if let linphoneFL = self.linphoneFriendList, linphoneFriend == linphoneFL.displayName { _ = linphoneFL.addFriend(linphoneFriend: friend) } else if let linphoneFL = self.tempRemoteFriendList { - _ = linphoneFL.addFriend(linphoneFriend: friend) + if friend.friendList?.type != .CardDAV { + _ = linphoneFL.addFriend(linphoneFriend: friend) + } } } else if existingFriend == nil { if let friendListTmp = self.friendList { @@ -338,10 +358,11 @@ final class ContactsManager: ObservableObject { do { let fileName = name + prefix + ".png" - let fileURL = directory.appendingPathComponent(fileName) + + let fileURL = directory.appendingPathComponent(fileName.replacingOccurrences(of: " ", with: "")) try data.write(to: fileURL) - completion(fileName) + completion(fileName.replacingOccurrences(of: " ", with: "")) } catch { print("Error writing image: \(error)") completion("") @@ -379,6 +400,12 @@ final class ContactsManager: ObservableObject { friend = tempRemoteFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) }) } + CoreContext.shared.mCore.friendsLists.forEach { friendList in + if friendList.type == .CardDAV { + friend = friendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) }) + } + } + return friend } @@ -396,6 +423,13 @@ final class ContactsManager: ObservableObject { friendList.updateSubscriptions() } + if let friendListDelegateToDelete = self.friendListDelegate { + CoreContext.shared.mCore.friendsLists.forEach { friendList in + friendList.removeDelegate(delegate: friendListDelegateToDelete) + } + } + self.friendListDelegate = nil + let friendListDelegateTmp = FriendListDelegateStub( onContactCreated: { (friendList: FriendList, linphoneFriend: Friend) in Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactCreated") @@ -407,7 +441,7 @@ final class ContactsManager: ObservableObject { Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactUpdated") }, onSyncStatusChanged: { (friendList: FriendList, status: FriendList.SyncStatus?, message: String?) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged") + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged \(friendList.displayName ?? "No Display Name") -- Status: \(status != nil ? String(describing: status!) : "No Status")") if status == .Successful { if friendList.displayName != self.nativeAddressBookFriendList && friendList.displayName != self.linphoneAddressBookFriendList { if let tempRemoteFriendList = self.tempRemoteFriendList { @@ -421,31 +455,42 @@ final class ContactsManager: ObservableObject { } let dispatchGroup = DispatchGroup() - friendList.friends.forEach { friend in dispatchGroup.enter() let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? "" let newContact = Contact( identifier: UUID().uuidString, - firstName: friend.name ?? addressTmp, - lastName: "", - organizationName: "", - jobTitle: "", + firstName: friend.firstName ?? addressTmp, + lastName: friend.lastName ?? "", + organizationName: friend.organization ?? "", + jobTitle: friend.jobTitle ?? "", displayName: friend.address?.displayName ?? "", sipAddresses: friend.addresses.map { $0.asStringUriOnly() }, - phoneNumbers: [], + phoneNumbers: friend.phoneNumbersWithLabel.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.phoneNumber)}, imageData: "" ) - let image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "") - self.saveImage( - image: image, - name: friend.name ?? addressTmp, - prefix: "-default", - contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: nil) { - dispatchGroup.leave() + let image: UIImage? + if let photo = friend.photo, !photo.isEmpty { + if let imageTmp = self.imageFromBase64(photo) { + image = imageTmp + } else { + image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "") } + } else { + image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "") + } + + if let image = image { + self.saveImage( + image: image, + name: friend.name ?? addressTmp, + prefix: "-default", + contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: friend.friendList?.type == .CardDAV ? friend : nil) { + dispatchGroup.leave() + } + } } dispatchGroup.notify(queue: .main) { @@ -561,6 +606,11 @@ final class ContactsManager: ObservableObject { func addCoreDelegate(core: Core) { self.coreContext.doOnCoreQueue { _ in + if let coreDelegate = self.coreDelegate { + core.removeDelegate(delegate: coreDelegate) + self.coreDelegate = nil + } + self.coreDelegate = CoreDelegateStub( onFriendListCreated: { (_: Core, friendList: FriendList) in Log.info("\(ContactsManager.TAG) Friend list \(friendList.displayName) created") @@ -602,6 +652,17 @@ final class ContactsManager: ObservableObject { } } } + + func refreshCardDavContacts() { + self.coreContext.doOnCoreQueue { core in + core.friendsLists.forEach{ friendList in + if (friendList.type == .CardDAV) { + Log.info("\(ContactsManager.TAG) Found CardDAV friend list \(friendList.displayName), starting update") + friendList.synchronizeFriendsFromServer() + } + } + } + } } struct PhoneNumber { diff --git a/Linphone/Core/CorePreferences.swift b/Linphone/Core/CorePreferences.swift index d474afd27..0d205e9d3 100644 --- a/Linphone/Core/CorePreferences.swift +++ b/Linphone/Core/CorePreferences.swift @@ -150,6 +150,15 @@ class CorePreferences { } } + static var friendListInWhichStoreNewlyCreatedFriends: String { + get { + return Config.get().getString(section: "app", key: "friend_list_to_store_newly_created_contacts", defaultString: "Linphone address-book") + } + set { + Config.get().setString(section: "app", key: "friend_list_to_store_newly_created_contacts", value: newValue) + } + } + static var voiceRecordingMaxDuration: Int { get { return Config.get().getInt(section: "app", key: "voice_recording_max_duration", defaultValue: 600000) diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 30849b247..78479300d 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -467,15 +467,22 @@ "settings_contacts_carddav_username_title" = "Username"; "settings_contacts_edit_carddav_server_title" = "Edit CardDAV address book"; "settings_contacts_edit_ldap_server_title" = "Edit LDAP server"; +"settings_contacts_ldap_enabled_title" = "Enabled"; +"settings_contacts_ldap_server_url_title" = "Server URL (can't be empty)"; "settings_contacts_ldap_bind_dn_title" = "Bind DN"; -"settings_contacts_ldap_bind_user_password_title" = "Bind user password"; -"settings_contacts_ldap_max_results_title" = "Maximum results"; "settings_contacts_ldap_password_title" = "Password"; -"settings_contacts_ldap_request_timeout_title" = "Request timeout"; +"settings_contacts_ldap_use_tls_title" = "Use TLS"; "settings_contacts_ldap_search_base_title" = "Search base (can't be empty)"; "settings_contacts_ldap_search_filter_title" = "Filter"; -"settings_contacts_ldap_server_url_title" = "Server URL (can't be empty)"; -"settings_contacts_ldap_use_tls_title" = "Use TLS"; +"settings_contacts_ldap_max_results_title" = "Max results"; +"settings_contacts_ldap_request_timeout_title" = "Timeout (in seconds)"; +"settings_contacts_ldap_request_delay_title" = "Delay between two queries (in milliseconds)"; +"settings_contacts_ldap_min_characters_title" = "Min characters to start a query"; +"settings_contacts_ldap_name_attributes_title" = "Name attributes"; +"settings_contacts_ldap_sip_attributes_title" = "SIP attributes"; +"settings_contacts_ldap_sip_domain_title" = "SIP domain"; +"settings_contacts_ldap_error_toast" = "A error occurred, LDAP server not saved!"; +"settings_contacts_ldap_empty_server_error_toast" = "Server URL can't be empty"; "settings_contacts_title" = "Contacts"; "settings_conversations_auto_download_title" = "Auto-download files"; "settings_conversations_mark_as_read_when_dismissing_notif_title" = "Mark conversation as read when dismissing message notification"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 363d9c56b..58961a458 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -467,15 +467,22 @@ "settings_contacts_carddav_username_title" = "Nom d'utilisateur"; "settings_contacts_edit_carddav_server_title" = "Editer le carnet d'adresse CardDAV"; "settings_contacts_edit_ldap_server_title" = "Editer le serveur LDAP"; +"settings_contacts_ldap_enabled_title" = "Activé"; +"settings_contacts_ldap_server_url_title" = "URL du serveur (ne peut être vide)"; "settings_contacts_ldap_bind_dn_title" = "Bind DN"; -"settings_contacts_ldap_bind_user_password_title" = "Mot de passe de l'utilisateur Bind"; -"settings_contacts_ldap_max_results_title" = "Nombre de résultats maximum"; "settings_contacts_ldap_password_title" = "Mot de passe"; -"settings_contacts_ldap_request_timeout_title" = "Délai d'attente de la requête"; +"settings_contacts_ldap_use_tls_title" = "Utiliser TLS"; "settings_contacts_ldap_search_base_title" = "Base de recherche (ne peut être vide)"; "settings_contacts_ldap_search_filter_title" = "Filtre"; -"settings_contacts_ldap_server_url_title" = "URL du serveur (ne peut être vide)"; -"settings_contacts_ldap_use_tls_title" = "Utiliser TLS"; +"settings_contacts_ldap_max_results_title" = "Nombre de résultats maximum"; +"settings_contacts_ldap_request_timeout_title" = "Durée maximum (en secondes)"; +"settings_contacts_ldap_request_delay_title" = "Délai entre 2 requêtes (en millisecondes)"; +"settings_contacts_ldap_min_characters_title" = "Nombre minimum de caractères pour lancer la requête"; +"settings_contacts_ldap_name_attributes_title" = "Attributs de nom"; +"settings_contacts_ldap_sip_attributes_title" = "Attributs SIP"; +"settings_contacts_ldap_sip_domain_title" = "Domaine SIP"; +"settings_contacts_ldap_error_toast" = "Une erreur s'est produite, la configuration LDAP n'a pas été sauvegardée !"; +"settings_contacts_ldap_empty_server_error_toast" = "L'URL du serveur ne peut être vide"; "settings_contacts_title" = "Contacts"; "settings_conversations_auto_download_title" = "Télécharger automatiquement les fichiers"; "settings_conversations_mark_as_read_when_dismissing_notif_title" = "Marquer la conversation comme lue lorsqu'une notification de message est supprimée"; diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift index 47a885ec4..defc2f8cd 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift @@ -80,6 +80,9 @@ struct ContactsInnerFragment: View { .frame(height: 12) }) .listStyle(.plain) + .refreshable { + contactsManager.refreshCardDavContacts() + } .overlay( VStack { if contactsManager.avatarListModel.isEmpty { diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift index 4946e078c..56414b9f2 100644 --- a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift @@ -35,6 +35,10 @@ class ContactsListViewModel: ObservableObject { private var contactChatRoomDelegate: ChatRoomDelegate? + private let nativeAddressBookFriendList = "Native address-book" + let linphoneAddressBookFriendList = "Linphone address-book" + let tempRemoteAddressBookFriendList = "TempRemoteDirectoryContacts address-book" + init() {} func createOneToOneChatRoomWith(remote: Address) { diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index d61e77c97..6a063eff7 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -315,6 +315,27 @@ struct ToastView: View { .foregroundStyle(Color.redDanger500) .default_text_style(styleSize: 15) .padding(8) + + case "Success_settings_contacts_carddav_sync_successful_toast": + Text("settings_contacts_carddav_sync_successful_toast") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + + case "settings_contacts_carddav_sync_error_toast": + Text("settings_contacts_carddav_sync_error_toast") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Success_settings_contacts_carddav_deleted_toast": + Text("settings_contacts_carddav_deleted_toast") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) default: Text("Error") diff --git a/Linphone/UI/Main/Settings/Fragments/CardDavAddressBookConfigurationFragment.swift b/Linphone/UI/Main/Settings/Fragments/CardDavAddressBookConfigurationFragment.swift new file mode 100644 index 000000000..cb509603a --- /dev/null +++ b/Linphone/UI/Main/Settings/Fragments/CardDavAddressBookConfigurationFragment.swift @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2010-2025 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI + +struct CardDavAddressBookConfigurationFragment: View { + @StateObject private var cardDavViewModel: CardDavViewModel + + @EnvironmentObject var settingsViewModel: SettingsViewModel + + @Environment(\.dismiss) var dismiss + + @State var isSecured: Bool = true + + @FocusState var isDisplayNameFocused: Bool + @FocusState var isServerUrlFocused: Bool + @FocusState var isUsernameFocused: Bool + @FocusState var isPasswordFocused: Bool + @FocusState var isRealmFocused: Bool + + init(name: String? = "") { + _cardDavViewModel = StateObject(wrappedValue: CardDavViewModel(name: name)) + } + + 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 { + dismiss() + } + + Text(cardDavViewModel.isEdit ? "settings_contacts_edit_carddav_server_title" : "settings_contacts_add_carddav_server_title") + .default_text_style_orange_800(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + + Spacer() + + if cardDavViewModel.isEdit { + Button { + cardDavViewModel.delete() + } label: { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .frame(width: 28, height: 28) + .foregroundStyle(.red) + .padding(.horizontal, 5) + .padding(.top, 4) + } + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + VStack(spacing: 30) { + VStack(alignment: .leading) { + Text("settings_contacts_carddav_name_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_carddav_name_title", text: $cardDavViewModel.displayName) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isDisplayNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isDisplayNameFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_carddav_server_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_carddav_server_url_title", text: $cardDavViewModel.serverUrl) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isServerUrlFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isServerUrlFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_carddav_username_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_carddav_username_title", text: $cardDavViewModel.username) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isUsernameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isUsernameFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_carddav_password_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + ZStack(alignment: .trailing) { + Group { + if isSecured { + SecureField("settings_contacts_carddav_password_title", text: $cardDavViewModel.password) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isPasswordFocused) + } else { + TextField("settings_contacts_carddav_password_title", text: $cardDavViewModel.password) + .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) + .frame(height: 25) + .focused($isPasswordFocused) + } + } + + 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(isPasswordFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.bottom) + } + + VStack(alignment: .leading) { + Text("settings_contacts_carddav_realm_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_carddav_realm_title", text: $cardDavViewModel.realm) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isRealmFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isRealmFocused) + } + + Toggle("settings_contacts_carddav_use_as_default_title", isOn: $cardDavViewModel.storeNewContactsInIt) + .default_text_style_700(styleSize: 15) + + } + .padding(.vertical, 30) + .padding(.horizontal, 20) + .background(Color.gray100) + } + } + .background(Color.gray100) + } + .background(Color.gray100) + + VStack { + Spacer() + HStack { + Spacer() + + Button { + cardDavViewModel.addAddressBook() + } label: { + Image(cardDavViewModel.isEdit ? "check" : "plus-circle") + .renderingMode(.template) + .resizable() + .frame(width: 28, height: 28) + .foregroundStyle(.white) + .padding() + .background(cardDavViewModel.isFormComplete ? Color.orangeMain500 : Color.gray300) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + + } + .padding() + .disabled(!cardDavViewModel.isFormComplete) + } + } + + if cardDavViewModel.cardDavServerOperationInProgress { + PopupLoadingView() + .background(.black.opacity(0.65)) + } + } + .onChange(of: cardDavViewModel.cardDavServerOperationSuccessful) { event in + if event { + dismiss() + settingsViewModel.reloadConfiguredCardDavServers() + } + } + .navigationTitle("") + .navigationBarHidden(true) + } +} diff --git a/Linphone/UI/Main/Settings/Fragments/LdapServerConfigurationFragment.swift b/Linphone/UI/Main/Settings/Fragments/LdapServerConfigurationFragment.swift new file mode 100644 index 000000000..2404ef40a --- /dev/null +++ b/Linphone/UI/Main/Settings/Fragments/LdapServerConfigurationFragment.swift @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2010-2025 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI + +struct LdapServerConfigurationFragment: View { + @StateObject private var ldapViewModel: LdapViewModel + + @EnvironmentObject var settingsViewModel: SettingsViewModel + + @Environment(\.dismiss) var dismiss + + @State var isSecured: Bool = true + + @FocusState var isServerUrlFocused: Bool + @FocusState var isBindDnFocused: Bool + @FocusState var isPasswordFocused: Bool + @FocusState var isSearchBaseFocused: Bool + @FocusState var isSearchFilterFocused: Bool + @FocusState var isMaxResultsFocused: Bool + @FocusState var isRequestTimeoutFocused: Bool + @FocusState var isRequestDelayFocused: Bool + @FocusState var isMinCharactersFocused: Bool + @FocusState var isNameAttributesFocused: Bool + @FocusState var isSipAttributesFocused: Bool + @FocusState var isSipDomainFocused: Bool + + init(url: String? = "") { + _ldapViewModel = StateObject(wrappedValue: LdapViewModel(url: url)) + } + + 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 { + dismiss() + } + + Text(ldapViewModel.isEdit ? "settings_contacts_edit_ldap_server_title" : "settings_contacts_add_ldap_server_title") + .default_text_style_orange_800(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + + Spacer() + + if ldapViewModel.isEdit { + Button { + ldapViewModel.delete() + } label: { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .frame(width: 28, height: 28) + .foregroundStyle(.red) + .padding(.horizontal, 5) + .padding(.top, 4) + } + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + VStack(spacing: 30) { + Toggle("settings_contacts_ldap_enabled_title", isOn: $ldapViewModel.isEnabled) + .default_text_style_700(styleSize: 15) + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_server_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_server_url_title", text: $ldapViewModel.serverUrl) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isServerUrlFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isServerUrlFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_bind_dn_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_bind_dn_title", text: $ldapViewModel.bindDn) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isBindDnFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isBindDnFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_password_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + ZStack(alignment: .trailing) { + Group { + if isSecured { + SecureField("settings_contacts_ldap_password_title", text: $ldapViewModel.password) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isPasswordFocused) + } else { + TextField("settings_contacts_ldap_password_title", text: $ldapViewModel.password) + .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) + .frame(height: 25) + .focused($isPasswordFocused) + } + } + + 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(isPasswordFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.bottom) + } + + Toggle("settings_contacts_ldap_use_tls_title", isOn: $ldapViewModel.useTls) + .default_text_style_700(styleSize: 15) + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_search_base_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_search_base_title", text: $ldapViewModel.searchBase) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSearchBaseFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isSearchBaseFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_search_filter_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_search_filter_title", text: $ldapViewModel.searchFilter) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSearchFilterFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isSearchFilterFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_max_results_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_max_results_title", text: $ldapViewModel.maxResults) + .default_text_style(styleSize: 15) + .keyboardType(.numberPad) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isMaxResultsFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isMaxResultsFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_request_timeout_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_request_timeout_title", text: $ldapViewModel.requestTimeout) + .default_text_style(styleSize: 15) + .keyboardType(.numberPad) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isRequestTimeoutFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isRequestTimeoutFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_request_delay_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_request_delay_title", text: $ldapViewModel.requestDelay) + .default_text_style(styleSize: 15) + .keyboardType(.numberPad) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isRequestDelayFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isRequestDelayFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_min_characters_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_min_characters_title", text: $ldapViewModel.minCharacters) + .default_text_style(styleSize: 15) + .keyboardType(.numberPad) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isMinCharactersFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isMinCharactersFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_name_attributes_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_name_attributes_title", text: $ldapViewModel.nameAttributes) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isNameAttributesFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isNameAttributesFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_sip_attributes_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_sip_attributes_title", text: $ldapViewModel.sipAttributes) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSipAttributesFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isSipAttributesFocused) + } + + VStack(alignment: .leading) { + Text("settings_contacts_ldap_sip_domain_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("settings_contacts_ldap_sip_domain_title", text: $ldapViewModel.sipDomain) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSipDomainFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isSipDomainFocused) + } + } + .padding(.vertical, 30) + .padding(.horizontal, 20) + .background(Color.gray100) + } + } + .background(Color.gray100) + } + .background(Color.gray100) + + VStack { + Spacer() + HStack { + Spacer() + + Button { + ldapViewModel.addServer() + } label: { + Image("plus-circle") + .renderingMode(.template) + .resizable() + .frame(width: 28, height: 28) + .foregroundStyle(.white) + .padding() + .background(ldapViewModel.isFormComplete ? Color.orangeMain500 : Color.gray300) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + + } + .padding() + .disabled(!ldapViewModel.isFormComplete) + } + } + } + .onChange(of: ldapViewModel.ldapServerOperationSuccessful) { event in + if event { + dismiss() + settingsViewModel.reloadLdapServers() + } + } + .navigationTitle("") + .navigationBarHidden(true) + } +} diff --git a/Linphone/UI/Main/Settings/Fragments/SettingsFragment.swift b/Linphone/UI/Main/Settings/Fragments/SettingsFragment.swift index 5a1f0589d..706260607 100644 --- a/Linphone/UI/Main/Settings/Fragments/SettingsFragment.swift +++ b/Linphone/UI/Main/Settings/Fragments/SettingsFragment.swift @@ -245,9 +245,6 @@ struct SettingsFragment: View { .transition(.move(edge: .top)) } - /* - // Hide Contacts - HStack(alignment: .center) { Text("settings_contacts_title") .default_text_style_800(styleSize: 18) @@ -273,12 +270,100 @@ struct SettingsFragment: View { if contactsIsOpen { VStack(spacing: 0) { - VStack(spacing: 30) { - Toggle("account_settings_avpf_title", isOn: $isOn) - .default_text_style_700(styleSize: 15) + VStack(spacing: 20) { + NavigationLink(destination: { + LdapServerConfigurationFragment() + .environmentObject(settingsViewModel) + }, label: { + HStack(alignment: .center) { + Text("settings_contacts_add_ldap_server_title") + .default_text_style_700(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image("caret-right") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20, alignment: .leading) + .padding(.all, 10) + } + .frame(maxWidth: .infinity) + }) - Toggle("account_settings_avpf_title", isOn: $isOn) - .default_text_style_700(styleSize: 15) + if !settingsViewModel.ldapServers.isEmpty { + ForEach(settingsViewModel.ldapServers, id: \.self) { ldap in + NavigationLink(destination: { + LdapServerConfigurationFragment(url: ldap) + .environmentObject(settingsViewModel) + }, label: { + HStack(alignment: .center) { + Text(ldap) + .default_text_style_700(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20, alignment: .leading) + .padding(.all, 10) + } + .padding(.horizontal, 10) + .frame(maxWidth: .infinity) + }) + } + } + + NavigationLink(destination: { + CardDavAddressBookConfigurationFragment() + .environmentObject(settingsViewModel) + }, label: { + HStack(alignment: .center) { + Text("settings_contacts_add_carddav_server_title") + .default_text_style_700(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image("caret-right") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20, alignment: .leading) + .padding(.all, 10) + } + .frame(maxWidth: .infinity) + }) + + if !settingsViewModel.cardDavFriendsLists.isEmpty { + ForEach(settingsViewModel.cardDavFriendsLists, id: \.self) { cardDavName in + NavigationLink(destination: { + CardDavAddressBookConfigurationFragment(name: cardDavName) + .environmentObject(settingsViewModel) + }, label: { + HStack(alignment: .center) { + Text(cardDavName) + .default_text_style_700(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20, alignment: .leading) + .padding(.all, 10) + } + .padding(.horizontal, 10) + .frame(maxWidth: .infinity) + }) + } + } } .padding(.vertical, 30) .padding(.horizontal, 20) @@ -289,7 +374,6 @@ struct SettingsFragment: View { .zIndex(-4) .transition(.move(edge: .top)) } - */ HStack(alignment: .center) { Text("settings_meetings_title") diff --git a/Linphone/UI/Main/Settings/Fragments/Untitled.swift b/Linphone/UI/Main/Settings/Fragments/Untitled.swift new file mode 100644 index 000000000..6de936bfb --- /dev/null +++ b/Linphone/UI/Main/Settings/Fragments/Untitled.swift @@ -0,0 +1,7 @@ +// +// Untitled.swift +// LinphoneApp +// +// Created by Benoît Martins on 06/10/2025. +// + diff --git a/Linphone/UI/Main/Settings/ViewModel/CardDavViewModel.swift b/Linphone/UI/Main/Settings/ViewModel/CardDavViewModel.swift new file mode 100644 index 000000000..7c10aa8d1 --- /dev/null +++ b/Linphone/UI/Main/Settings/ViewModel/CardDavViewModel.swift @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2010-2025 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 Foundation +import Combine +import SwiftUI +import linphonesw + +class CardDavViewModel: ObservableObject { + static let TAG = "[CardDAV ViewModel]" + + private var coreContext = CoreContext.shared + + let linphoneAddressBookFriendList = "Linphone address-book" + let tempRemoteAddressBookFriendList = "TempRemoteDirectoryContacts address-book" + + @Published var isEdit: Bool = false + @Published var displayName: String = "" + @Published var serverUrl: String = "" + @Published var username: String = "" + @Published var password: String = "" + @Published var realm: String = "" + @Published var storeNewContactsInIt: Bool = false + @Published var isReadOnly: Bool = false + + var isFormComplete: Bool { + !displayName.isEmpty && + !serverUrl.isEmpty && + !username.isEmpty && + !realm.isEmpty + } + + @Published var cardDavServerOperationInProgress = false + @Published var cardDavServerOperationSuccessful = false + + private var friendList: FriendList? + private var friendListDelegate: FriendListDelegate? + + init(name: String? = "") { + isEdit = false + cardDavServerOperationInProgress = false + storeNewContactsInIt = false + + if let name = name, !name.isEmpty { + loadcardDav(name: name) + } + } + + deinit { + if let friendList = self.friendList, let friendListDelegate = self.friendListDelegate { + self.coreContext.doOnCoreQueue { core in + friendList.removeDelegate(delegate: friendListDelegate) + } + } + } + + func loadcardDav(name: String) { + self.coreContext.doOnCoreQueue { core in + let found = core.getFriendListByName(name: name) + guard let found = found else { + Log.error("\(CardDavViewModel.TAG) Failed to find friend list with display name \(name)!") + return + } + + self.friendList = found + + guard let friendList = self.friendList else { + return + } + + let isReadOnlyTmp = friendList.isReadOnly + let friendListInWhichStoreNewlyCreatedFriendsTmp = CorePreferences.friendListInWhichStoreNewlyCreatedFriends + let uriTmp = friendList.uri ?? "" + DispatchQueue.main.async { + self.isEdit = true + self.isReadOnly = isReadOnlyTmp + + self.displayName = name + self.storeNewContactsInIt = name == friendListInWhichStoreNewlyCreatedFriendsTmp + + self.serverUrl = uriTmp + } + Log.info("\(CardDavViewModel.TAG) Existing friend list CardDAV values loaded") + } + } + + func delete() { + self.coreContext.doOnCoreQueue { core in + if self.isEdit, let friendList = self.friendList { + let name = friendList.displayName + if name == CorePreferences.friendListInWhichStoreNewlyCreatedFriends { + Log.info("\(CardDavViewModel.TAG) Deleting friend list configured to be used to store newly created friends, updating default friend list back to \(self.linphoneAddressBookFriendList)") + CorePreferences.friendListInWhichStoreNewlyCreatedFriends = self.linphoneAddressBookFriendList + } + + if let tempRemoteFriendList = core.getFriendListByName(name: self.tempRemoteAddressBookFriendList) { + tempRemoteFriendList.friends.forEach { friend in + if let friendAddress = friend.address, + friendList.friends.contains(where: { $0.address?.weakEqual(address2: friendAddress) == true }) { + _ = tempRemoteFriendList.removeFriend(linphoneFriend: friend) + } + } + } + + core.removeFriendList(list: friendList) + Log.info("\(CardDavViewModel.TAG) Removed friends list with display name \(name ?? "")") + + Log.info("\(CardDavViewModel.TAG) Notifying contacts manager that contacts have changed") + MagicSearchSingleton.shared.searchForContacts() + + DispatchQueue.main.async { + NotificationCenter.default.post(name: NSNotification.Name("ContactLoaded"), object: nil) + self.cardDavServerOperationSuccessful = true + + ToastViewModel.shared.toastMessage = "Success_settings_contacts_carddav_deleted_toast" + ToastViewModel.shared.displayToast = true + } + } + } + } + + func addAddressBook() { + let name = displayName + let server = serverUrl + if name.isEmpty || server.isEmpty { + return + } + + let user = username + let pwd = password + let authRealm = realm + + self.coreContext.doOnCoreQueue { core in + // TODO: add dialog to ask user before removing existing friend list & auth info ? + if !self.isEdit == false { + let foundFriendList = core.getFriendListByName(name: name) + if let foundFriendList = foundFriendList { + Log.warn("\(CardDavViewModel.TAG) Friend list \(name) already exists, removing it first") + core.removeFriendList(list: foundFriendList) + } + } + + if !user.isEmpty && !authRealm.isEmpty { + let foundAuthInfo = core.findAuthInfo(realm: authRealm, username: user, sipDomain: nil) + if let foundAuthInfo = foundAuthInfo { + Log.warn("\(CardDavViewModel.TAG) Auth info with username \(user) already exists, removing it first") + core.removeAuthInfo(info: foundAuthInfo) + } + + Log.info("\(CardDavViewModel.TAG) Adding auth info with username \(user)") + if let authInfo = try? Factory.Instance.createAuthInfo( + username: user, + userid: nil, + passwd: pwd, + ha1: nil, + realm: authRealm, + domain: nil + ) { + core.addAuthInfo(info: authInfo) + } + } + + if self.isEdit && self.friendList != nil { + Log.info("\(CardDavViewModel.TAG) Changes were made to CardDAV friend list \(name), synchronizing it") + } else { + self.friendList = try? core.createFriendList() + + guard let friendList = self.friendList else { + Log.error("\(CardDavViewModel.TAG) Failed to create CardDAV friend list") + return + } + + friendList.displayName = name + friendList.type = .CardDAV + friendList.uri = if (server.hasPrefix("http://") || server.hasPrefix("https://")) { + server + } else { + "https://$server" + } + friendList.databaseStorageEnabled = true + + self.addFriendListDelegate(friendList: friendList) + + core.addFriendList(list: friendList) + + Log.info("\(CardDavViewModel.TAG) CardDAV friend list \(name) created with server URL \(server), synchronizing it") + } + + if !self.storeNewContactsInIt && CorePreferences.friendListInWhichStoreNewlyCreatedFriends == name { + Log.info("\(CardDavViewModel.TAG) No longer using friend list \(name) as default friend list, switching back to \(self.linphoneAddressBookFriendList)") + CorePreferences.friendListInWhichStoreNewlyCreatedFriends = self.linphoneAddressBookFriendList + } + + if let friendList = self.friendList { + friendList.synchronizeFriendsFromServer() + } + + DispatchQueue.main.async { + self.cardDavServerOperationInProgress = true + } + } + } + + func addFriendListDelegate(friendList: FriendList) { + self.coreContext.doOnCoreQueue { core in + let delegate = FriendListDelegateStub( + onSyncStatusChanged: { (friendList: FriendList, status: FriendList.SyncStatus, message: String?) in + Log.info("\(CardDavViewModel.TAG) Friend list \(friendList.displayName ?? "") sync status changed to \(status) with message \(message ?? "")") + switch status { + case .Successful: + DispatchQueue.main.async { + self.cardDavServerOperationInProgress = false + + ToastViewModel.shared.toastMessage = "Success_settings_contacts_carddav_sync_successful_toast" + ToastViewModel.shared.displayToast = true + } + + let name = self.displayName + if self.storeNewContactsInIt { + let previous = CorePreferences.friendListInWhichStoreNewlyCreatedFriends + if friendList.isReadOnly { + Log.warn("\(CardDavViewModel.TAG) User asked to add newly created contacts in this friend list but it is read only, keep currently default friend list \(previous)") + self.storeNewContactsInIt = false + } else { + Log.info("\(CardDavViewModel.TAG) Updating default friend list to store newly created contacts from \(previous) to \(name)") + CorePreferences.friendListInWhichStoreNewlyCreatedFriends = name + } + self.isReadOnly = friendList.isReadOnly + } + + Log.info("\(CardDavViewModel.TAG) Notifying contacts manager that contacts have changed") + + DispatchQueue.main.async { + self.cardDavServerOperationSuccessful = true + } + case .Failure: + DispatchQueue.main.async { + self.cardDavServerOperationInProgress = false + + ToastViewModel.shared.toastMessage = "settings_contacts_carddav_sync_error_toast" + ToastViewModel.shared.displayToast = true + } + if !self.isEdit { + Log.error("\(CardDavViewModel.TAG) Synchronization failed, removing Friend list from Core") + if let friendListDelegate = self.friendListDelegate { + friendList.removeDelegate(delegate: friendListDelegate) + } + core.removeFriendList(list: friendList) + } + default: break + } + + }) + + self.friendListDelegate = delegate + + if let friendList = self.friendList { + friendList.addDelegate(delegate: delegate) + } + } + } +} + diff --git a/Linphone/UI/Main/Settings/ViewModel/LdapViewModel.swift b/Linphone/UI/Main/Settings/ViewModel/LdapViewModel.swift new file mode 100644 index 000000000..8bc7857c9 --- /dev/null +++ b/Linphone/UI/Main/Settings/ViewModel/LdapViewModel.swift @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2010-2025 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 Foundation +import Combine +import SwiftUI +import linphonesw + +class LdapViewModel: ObservableObject { + static let TAG = "[LDAP ViewModel]" + + private var coreContext = CoreContext.shared + + @Published var isEdit: Bool = false + @Published var isEnabled: Bool = true + @Published var serverUrl: String = "" + @Published var bindDn: String = "" + @Published var password: String = "" + @Published var useTls: Bool = true + @Published var searchBase: String = "" + @Published var searchFilter: String = "" + @Published var maxResults: String = "" + @Published var requestTimeout: String = "5" + @Published var requestDelay: String = "2000" + @Published var minCharacters: String = "3" + @Published var nameAttributes: String = "" + @Published var sipAttributes: String = "" + @Published var sipDomain: String = "" + + var isFormComplete: Bool { + !serverUrl.isEmpty && + !bindDn.isEmpty && + !searchBase.isEmpty && + !searchFilter.isEmpty && + !maxResults.isEmpty && + !requestTimeout.isEmpty && + !requestDelay.isEmpty && + !minCharacters.isEmpty && + !nameAttributes.isEmpty && + !sipAttributes.isEmpty && + !sipDomain.isEmpty + } + + @Published var ldapServerOperationSuccessful = false + + private var ldapToEdit: Ldap? + + init(url: String? = "") { + isEdit = false + isEnabled = true + + useTls = true + minCharacters = "3" + requestTimeout = "5" + requestDelay = "2000" + + if let url = url, !url.isEmpty { + loadLdap(url: url) + } + } + + func loadLdap(url: String) { + self.coreContext.doOnCoreQueue { core in + if let found = core.ldapList.first(where: { $0.params?.server == url }) { + let isEditTmp = true + self.ldapToEdit = found + if let ldapParams = self.ldapToEdit?.params { + let isEnabledTmp = ldapParams.enabled + + let serverUrlTmp = ldapParams.server + let bindDnTmp = ldapParams.bindDn ?? "" + let useTlsTmp = ldapParams.tlsEnabled + let searchBaseTmp = ldapParams.baseObject + let searchFilterTmp = ldapParams.filter ?? "" + let maxResultsTmp = String(ldapParams.maxResults) + let requestTimeoutTmp = String(ldapParams.timeout) + let requestDelayTmp = String(ldapParams.delay) + let minCharactersTmp = String(ldapParams.minChars) + let nameAttributesTmp = ldapParams.nameAttribute ?? "" + let sipAttributesTmp = ldapParams.sipAttribute ?? "" + let sipDomainTmp = ldapParams.sipDomain ?? "" + Log.info("\(LdapViewModel.TAG) Existing LDAP server values loaded") + + DispatchQueue.main.async { + self.isEdit = isEditTmp + self.isEnabled = isEnabledTmp + + self.serverUrl = serverUrlTmp + self.bindDn = bindDnTmp + self.useTls = useTlsTmp + self.searchBase = searchBaseTmp + self.searchFilter = searchFilterTmp + self.maxResults = maxResultsTmp + self.requestTimeout = requestTimeoutTmp + self.requestDelay = requestDelayTmp + self.minCharacters = minCharactersTmp + self.nameAttributes = nameAttributesTmp + self.sipAttributes = sipAttributesTmp + self.sipDomain = sipDomainTmp + } + } + } else { + print("\(LdapViewModel.TAG) Failed to find LDAP server with URL \(url)!") + return + } + } + } + + func delete() { + self.coreContext.doOnCoreQueue { core in + if let ldapToEdit = self.ldapToEdit { + if self.isEdit { + let serverUrl = ldapToEdit.params?.server + core.removeLdap(ldap: ldapToEdit) + Log.info("\(LdapViewModel.TAG) Removed LDAP config for server URL \(serverUrl)") + + DispatchQueue.main.async { + self.ldapServerOperationSuccessful = true + } + } + } + } + } + + func addServer() { + self.coreContext.doOnCoreQueue { core in + do { + let server = self.serverUrl + if server.isEmpty { + Log.error("\(LdapViewModel.TAG) Server field can't be empty!") + return + } + + let ldapParams = try core.createLdapParams() + + ldapParams.enabled = self.isEnabled == true + ldapParams.server = server + ldapParams.bindDn = self.bindDn + ldapParams.password = self.password + ldapParams.authMethod = Ldap.AuthMethod.Simple + ldapParams.tlsEnabled = self.useTls == true + ldapParams.serverCertificatesVerificationMode = Ldap.CertVerificationMode.Default + ldapParams.baseObject = self.searchBase + ldapParams.filter = self.searchFilter + ldapParams.maxResults = Int(self.maxResults) ?? 0 + ldapParams.timeout = Int(self.requestTimeout) ?? 0 + ldapParams.delay = Int(self.requestDelay) ?? 0 + ldapParams.minChars = Int(self.minCharacters) ?? 0 + ldapParams.nameAttribute = self.nameAttributes + ldapParams.sipAttribute = self.sipAttributes + ldapParams.sipDomain = self.sipDomain + ldapParams.debugLevel = Ldap.DebugLevel.Verbose + + if self.isEdit && self.ldapToEdit != nil { + self.ldapToEdit?.params = ldapParams + Log.info("\(LdapViewModel.TAG) LDAP changes have been applied") + } else { + let ldap = try core.createLdapWithParams(params: ldapParams) + core.addLdap(ldap: ldap) + Log.info("\(LdapViewModel.TAG) New LDAP config created") + } + + DispatchQueue.main.async { + self.ldapServerOperationSuccessful = true + } + } catch let error { + Log.error("\(LdapViewModel.TAG) Exception while creating LDAP: \(error)") + } + } + } +} + diff --git a/Linphone/UI/Main/Settings/ViewModel/SettingsViewModel.swift b/Linphone/UI/Main/Settings/ViewModel/SettingsViewModel.swift index 61a110e41..6b6958b0b 100644 --- a/Linphone/UI/Main/Settings/ViewModel/SettingsViewModel.swift +++ b/Linphone/UI/Main/Settings/ViewModel/SettingsViewModel.swift @@ -38,6 +38,10 @@ class SettingsViewModel: ObservableObject { // Conversations settings @Published var autoDownload: Bool = false + // Contacts settings + @Published var ldapServers: [String] = [] + @Published var cardDavFriendsLists: [String] = [] + // Meetings settings @Published var defaultLayout: String = "" @@ -141,6 +145,8 @@ class SettingsViewModel: ObservableObject { core.addDelegate(delegate: self.coreDelegate!) */ + self.reloadLdapServers() + self.reloadConfiguredCardDavServers() self.setupCodecs() } } @@ -154,6 +160,43 @@ class SettingsViewModel: ObservableObject { } } + func reloadLdapServers() { + CoreContext.shared.doOnCoreQueue { core in + var list: [String] = [] + + core.ldapList.forEach({ ldap in + let label = ldap.params?.server ?? "" + if !label.isEmpty { + list.append(label) + } + }) + + DispatchQueue.main.async { + self.ldapServers = list + } + } + } + + func reloadConfiguredCardDavServers() { + CoreContext.shared.doOnCoreQueue { core in + var list: [String] = [] + + core.friendsLists.forEach({ friendList in + if friendList.type == .CardDAV { + let label = friendList.displayName ?? friendList.uri ?? "" + if !label.isEmpty { + list.append(label) + } + } + }) + + DispatchQueue.main.async { + self.cardDavFriendsLists = list + } + } + } + + func downloadAndApplyRemoteProvisioning() { Log.info("\(SettingsViewModel.TAG) Updating remote provisioning URI now and then download/apply it") diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift index 60185e5e1..7bf7aa227 100644 --- a/Linphone/Utils/Avatar.swift +++ b/Linphone/Utils/Avatar.swift @@ -77,14 +77,31 @@ struct Avatar: View { } } } else if !contactAvatarModel.name.isEmpty { - Image(uiImage: contactsManager.textToImage( - firstName: contactAvatarModel.name, - lastName: contactAvatarModel.name.components(separatedBy: " ").count > 1 - ? contactAvatarModel.name.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) + ZStack { + Image(uiImage: contactsManager.textToImage( + firstName: contactAvatarModel.name, + lastName: contactAvatarModel.name.components(separatedBy: " ").count > 1 + ? contactAvatarModel.name.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + + HStack { + Spacer() + VStack { + Spacer() + if !hidePresence && (contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy) { + Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy") + .resizable() + .frame(width: avatarSize/4, height: avatarSize/4) + .padding(.trailing, avatarSize == 50 || avatarSize == 35 ? 1 : 3) + .padding(.bottom, avatarSize == 50 || avatarSize == 35 ? 1 : 3) + } + } + } + .frame(width: avatarSize, height: avatarSize) + } } else { Image("profil-picture-default") .resizable() diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 281b2e297..d74e7035f 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -79,6 +79,8 @@ final class MagicSearchSingleton: ObservableObject { !lastSearchFriend.contains(where: { $0.phoneNumber == phoneNumber }) { lastSearchFriend.append(searchResult) } + } else if searchResult.friend != nil && (searchResult.hasSourceFlag(source: .RemoteCardDAV) || searchResult.friend?.friendList?.type == .CardDAV || searchResult.hasSourceFlag(source: .LdapServers)) { + lastSearchFriend.append(searchResult) } else { lastSearchSuggestions.append(searchResult) } @@ -106,7 +108,6 @@ final class MagicSearchSingleton: ObservableObject { sortedLastSearch.forEach { searchResult in if searchResult.friend != nil { if (searchResult.friend?.friendList?.displayName == self.nativeAddressBookFriendList || searchResult.friend?.friendList?.displayName == self.linphoneAddressBookFriendList || searchResult.friend?.friendList?.displayName == self.tempRemoteAddressBookFriendList) { - addedAvatarListModel.append( ContactAvatarModel( friend: searchResult.friend!, @@ -115,7 +116,25 @@ final class MagicSearchSingleton: ObservableObject { withPresence: true ) ) - } + } else if searchResult.hasSourceFlag(source: .RemoteCardDAV) || searchResult.friend?.friendList?.type == .CardDAV { + addedAvatarListModel.append( + ContactAvatarModel( + friend: searchResult.friend!, + name: searchResult.friend?.name ?? "", + address: searchResult.friend?.address?.clone()?.asStringUriOnly() ?? "", + withPresence: true + ) + ) + } else if searchResult.hasSourceFlag(source: .LdapServers) { + addedAvatarListModel.append( + ContactAvatarModel( + friend: searchResult.friend!, + name: searchResult.friend?.name ?? "", + address: searchResult.friend?.address?.clone()?.asStringUriOnly() ?? "", + withPresence: false + ) + ) + } } } @@ -178,8 +197,9 @@ final class MagicSearchSingleton: ObservableObject { magicSearch.getContactsListAsync( filter: self.currentFilter, domain: self.allContact ? "" : self.domainDefaultAccount, - sourceFlags: MagicSearch.Source.All.rawValue, - aggregation: MagicSearch.Aggregation.Friend) + sourceFlags: MagicSearch.Source.All.rawValue, //MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue | MagicSearch.Source.RemoteCardDAV.rawValue, + aggregation: MagicSearch.Aggregation.Friend + ) } } } diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index a3573d7a7..b580d4d3e 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -67,6 +67,9 @@ D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */; }; D70C82A52C85EDCA0087F43F /* ConversationForwardMessageFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */; }; D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70C82A62C85F5910087F43F /* ConversationForwardMessageViewModel.swift */; }; + D711B1302E8FCEDE00DF8C71 /* CardDavViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D711B12F2E8FCED900DF8C71 /* CardDavViewModel.swift */; }; + D711B1322E8FCF8800DF8C71 /* LdapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D711B1312E8FCF8600DF8C71 /* LdapViewModel.swift */; }; + D711B1342E93F18700DF8C71 /* LdapServerConfigurationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D711B1332E93F18300DF8C71 /* LdapServerConfigurationFragment.swift */; }; D714035B2BE11E00004BD8CA /* CallMediaEncryptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */; }; D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */; }; D714DE622C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714DE612C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift */; }; @@ -129,6 +132,7 @@ D759CB642C3FBD4200AC35E8 /* StartConversationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */; }; D759CB662C3FBE1D00AC35E8 /* StartConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */; }; D76005F62B0798B00054B79A /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76005F52B0798B00054B79A /* IntExtension.swift */; }; + D762102C2E97FDFD002E7999 /* CardDavAddressBookConfigurationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D762102B2E97FDF8002E7999 /* CardDavAddressBookConfigurationFragment.swift */; }; D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7702EF12AC7205000557C00 /* WelcomeView.swift */; }; D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; }; D77A080E2CB6BCAF0095D589 /* MessageConferenceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77A080D2CB6BCA10095D589 /* MessageConferenceInfo.swift */; }; @@ -291,6 +295,9 @@ D70C3B5E2E0ABAB900F3F938 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = Localizable/uk.lproj/Localizable.strings; sourceTree = ""; }; D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationForwardMessageFragment.swift; sourceTree = ""; }; D70C82A62C85F5910087F43F /* ConversationForwardMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationForwardMessageViewModel.swift; sourceTree = ""; }; + D711B12F2E8FCED900DF8C71 /* CardDavViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDavViewModel.swift; sourceTree = ""; }; + D711B1312E8FCF8600DF8C71 /* LdapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LdapViewModel.swift; sourceTree = ""; }; + D711B1332E93F18300DF8C71 /* LdapServerConfigurationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LdapServerConfigurationFragment.swift; sourceTree = ""; }; D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMediaEncryptionModel.swift; sourceTree = ""; }; D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewModel.swift; sourceTree = ""; }; D714DE612C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterCodeConfirmationFragment.swift; sourceTree = ""; }; @@ -356,6 +363,7 @@ D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartConversationFragment.swift; sourceTree = ""; }; D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartConversationViewModel.swift; sourceTree = ""; }; D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; + D762102B2E97FDF8002E7999 /* CardDavAddressBookConfigurationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDavAddressBookConfigurationFragment.swift; sourceTree = ""; }; D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; D77A080D2CB6BCA10095D589 /* MessageConferenceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageConferenceInfo.swift; sourceTree = ""; }; @@ -1031,6 +1039,8 @@ D7DC096B2CFA192F00A6D47C /* Fragments */ = { isa = PBXGroup; children = ( + D762102B2E97FDF8002E7999 /* CardDavAddressBookConfigurationFragment.swift */, + D711B1332E93F18300DF8C71 /* LdapServerConfigurationFragment.swift */, D78607702D36CB87009E6A7E /* SettingsAdvancedFragment.swift */, D732C38B2D311D2100F78100 /* SettingsFragment.swift */, D7C5003F2D27F16900DD53EC /* AccountSettingsFragment.swift */, @@ -1050,6 +1060,8 @@ D7DC096D2CFA194600A6D47C /* ViewModel */ = { isa = PBXGroup; children = ( + D711B1312E8FCF8600DF8C71 /* LdapViewModel.swift */, + D711B12F2E8FCED900DF8C71 /* CardDavViewModel.swift */, D756C8142D34FF8900A58F2F /* SettingsViewModel.swift */, D7C500412D2BE96E00DD53EC /* AccountSettingsViewModel.swift */, D7DC09702CFDBF8300A6D47C /* AccountProfileViewModel.swift */, @@ -1301,6 +1313,7 @@ D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */, D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, D78607712D36CB8A009E6A7E /* SettingsAdvancedFragment.swift in Sources */, + D762102C2E97FDFD002E7999 /* CardDavAddressBookConfigurationFragment.swift in Sources */, 66E50A492BD12B2300AD61CA /* MeetingsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, D717630D2BD7BD0E00464097 /* ParticipantsListFragment.swift in Sources */, @@ -1319,6 +1332,7 @@ D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */, C6A5A9482C10B6A30070FEA4 /* AuthState.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, + D711B1342E93F18700DF8C71 /* LdapServerConfigurationFragment.swift in Sources */, D70A26F02B7D02E6006CC8FC /* ConversationViewModel.swift in Sources */, 6613A0AE2BAEB7DF008923A4 /* MeetingFragment.swift in Sources */, D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */, @@ -1374,6 +1388,7 @@ D7DC09712CFDBF9A00A6D47C /* AccountProfileViewModel.swift in Sources */, D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */, D72343362AD037AF009AA24E /* ToastView.swift in Sources */, + D711B1302E8FCEDE00DF8C71 /* CardDavViewModel.swift in Sources */, D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */, D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */, D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */, @@ -1383,6 +1398,7 @@ D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, 66246C6A2C622AE900973E97 /* TimeZoneExtension.swift in Sources */, 66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */, + D711B1322E8FCF8800DF8C71 /* LdapViewModel.swift in Sources */, D7F5F6412C359F3B007FCF2F /* SipAddressesPopup.swift in Sources */, D72A9A052B9750A1000DC093 /* UIList.swift in Sources */, D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */, @@ -1908,7 +1924,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git"; requirement = { - branch = stable; + branch = alpha; kind = branch; }; }; diff --git a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1266786d3..0e27a7170 100644 --- a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -34,7 +34,7 @@ "location" : "https://github.com/Finalet/Elegant-Emoji-Picker", "state" : { "branch" : "main", - "revision" : "12c1a2be1adbe7a774ebdd2c48f02d95b8884df6" + "revision" : "598ff0a72198375d7317b61982fa8648d0ba3a44" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git", "state" : { - "branch" : "stable", - "revision" : "4d51a91278236d1a22d880b769397c94a2bb7b3e" + "branch" : "alpha", + "revision" : "3b79481215d235e3e7cd142bc7c7eda077f1d99a" } }, {