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"
}
},
{