Add Ldap and Cardav settings

This commit is contained in:
Benoit Martins 2025-10-07 17:59:49 +02:00
parent 4b3d99245f
commit 41f9db8199
18 changed files with 1485 additions and 52 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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";

View file

@ -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";

View file

@ -80,6 +80,9 @@ struct ContactsInnerFragment: View {
.frame(height: 12)
})
.listStyle(.plain)
.refreshable {
contactsManager.refreshCardDavContacts()
}
.overlay(
VStack {
if contactsManager.avatarListModel.isEmpty {

View file

@ -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) {

View file

@ -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")

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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")

View file

@ -0,0 +1,7 @@
//
// Untitled.swift
// LinphoneApp
//
// Created by Benoît Martins on 06/10/2025.
//

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)")
}
}
}
}

View file

@ -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")

View file

@ -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()

View file

@ -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
)
}
}
}

View file

@ -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 = "<group>"; };
D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationForwardMessageFragment.swift; sourceTree = "<group>"; };
D70C82A62C85F5910087F43F /* ConversationForwardMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationForwardMessageViewModel.swift; sourceTree = "<group>"; };
D711B12F2E8FCED900DF8C71 /* CardDavViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDavViewModel.swift; sourceTree = "<group>"; };
D711B1312E8FCF8600DF8C71 /* LdapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LdapViewModel.swift; sourceTree = "<group>"; };
D711B1332E93F18300DF8C71 /* LdapServerConfigurationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LdapServerConfigurationFragment.swift; sourceTree = "<group>"; };
D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMediaEncryptionModel.swift; sourceTree = "<group>"; };
D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewModel.swift; sourceTree = "<group>"; };
D714DE612C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterCodeConfirmationFragment.swift; sourceTree = "<group>"; };
@ -356,6 +363,7 @@
D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartConversationFragment.swift; sourceTree = "<group>"; };
D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartConversationViewModel.swift; sourceTree = "<group>"; };
D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = "<group>"; };
D762102B2E97FDF8002E7999 /* CardDavAddressBookConfigurationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDavAddressBookConfigurationFragment.swift; sourceTree = "<group>"; };
D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = "<group>"; };
D77A080D2CB6BCA10095D589 /* MessageConferenceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageConferenceInfo.swift; sourceTree = "<group>"; };
@ -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;
};
};

View file

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