Add edit contact view

This commit is contained in:
Benoit Martins 2023-10-30 17:00:24 +01:00
parent ac7f4da260
commit abd5461f54
19 changed files with 1643 additions and 702 deletions

View file

@ -47,6 +47,9 @@
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */; };
D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; };
D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; };
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; };
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */; };
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */; };
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */; };
D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */; };
D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */; };
@ -110,6 +113,9 @@
D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = "<group>"; };
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = "<group>"; };
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = "<group>"; };
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactViewModel.swift; sourceTree = "<group>"; };
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = "<group>"; };
D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = "<group>"; };
D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = "<group>"; };
@ -165,6 +171,7 @@
D717071F2AC5989C0037746F /* TextExtension.swift */,
D74C9D002ACB098C0021626A /* PermissionManager.swift */,
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */,
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -331,6 +338,7 @@
D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */,
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */,
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */,
D7C365092AF001C300FE6142 /* EditContactFragment.swift */,
);
path = Fragments;
sourceTree = "<group>";
@ -341,6 +349,7 @@
D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */,
D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */,
D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */,
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -541,15 +550,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */,
D71707202AC5989C0037746F /* TextExtension.swift in Sources */,
D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */,
D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */,
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */,
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */,
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */,
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */,
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */,
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */,
D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */,
D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */,
D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */,

View file

@ -1,10 +1,10 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3627_17791)">
<rect x="0" y="0" width="120" height="120" rx="60" fill="#DFECF2"/>
<path d="M59.9666 26.667C41.5666 26.667 26.6666 41.6003 26.6666 60.0003C26.6666 78.4003 41.5666 93.3337 59.9666 93.3337C78.4 93.3337 93.3333 78.4003 93.3333 60.0003C93.3333 41.6003 78.4 26.667 59.9666 26.667ZM60 86.667C45.2666 86.667 33.3333 74.7337 33.3333 60.0003C33.3333 45.267 45.2666 33.3337 60 33.3337C74.7333 33.3337 86.6666 45.267 86.6666 60.0003C86.6666 74.7337 74.7333 86.667 60 86.667ZM71.6666 56.667C74.4333 56.667 76.6666 54.4337 76.6666 51.667C76.6666 48.9003 74.4333 46.667 71.6666 46.667C68.9 46.667 66.6666 48.9003 66.6666 51.667C66.6666 54.4337 68.9 56.667 71.6666 56.667ZM48.3333 56.667C51.1 56.667 53.3333 54.4337 53.3333 51.667C53.3333 48.9003 51.1 46.667 48.3333 46.667C45.5666 46.667 43.3333 48.9003 43.3333 51.667C43.3333 54.4337 45.5666 56.667 48.3333 56.667ZM60 78.3337C67.7666 78.3337 74.3666 73.467 77.0333 66.667H42.9666C45.6333 73.467 52.2333 78.3337 60 78.3337Z" fill="#4E6074"/>
<rect x="0" y="0" width="256" height="256" rx="128" fill="#DFECF2"/>
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24ZM74.08,197.5a64,64,0,0,1,107.84,0,87.83,87.83,0,0,1-107.84,0ZM96,120a32,32,0,1,1,32,32A32,32,0,0,1,96,120Zm97.76,66.41a79.66,79.66,0,0,0-36.06-28.75,48,48,0,1,0-59.4,0,79.66,79.66,0,0,0-36.06,28.75,88,88,0,1,1,131.52,0Z" fill="#4E6074"/>
</g>
<defs>
<filter id="filter0_d_3627_17791" x="0" y="0" width="120" height="120" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_d_3627_17791" x="0" y="0" width="256" height="256" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -22,55 +22,76 @@ import Contacts
import SwiftUI
final class ContactsManager: ObservableObject {
static let shared = ContactsManager()
private var coreContext = CoreContext.shared
private var magicSearch = MagicSearchSingleton.shared
private let nativeAddressBookFriendList = "Native address-book"
let linphoneAddressBookFirendList = "Linphone address-book"
static let shared = ContactsManager()
private var coreContext = CoreContext.shared
private var magicSearch = MagicSearchSingleton.shared
private let nativeAddressBookFriendList = "Native address-book"
let linphoneAddressBookFirendList = "Linphone address-book"
@Published var friendList: FriendList?
private init() {
fetchContacts()
}
func fetchContacts() {
DispatchQueue.global().async {
if self.coreContext.mCore.globalState == GlobalState.Shutdown || self.coreContext.mCore.globalState == GlobalState.Off {
print("$TAG Core is being stopped or already destroyed, abort")
@Published var linphoneFriendList: FriendList?
private init() {
fetchContacts()
}
func fetchContacts() {
DispatchQueue.global().async {
if self.coreContext.mCore.globalState == GlobalState.Shutdown || self.coreContext.mCore.globalState == GlobalState.Off {
print("$TAG Core is being stopped or already destroyed, abort")
} else {
print("$TAG ${friends.size} friends created")
print("$TAG ${friends.size} friends created")
self.friendList = self.coreContext.mCore.getFriendListByName(name: self.nativeAddressBookFriendList)
if self.friendList == nil {
do {
do {
self.friendList = try self.coreContext.mCore.createFriendList()
} catch let error {
print("Failed to enumerate contact", error)
}
}
} catch let error {
print("Failed to enumerate contact", error)
}
}
if self.friendList!.displayName == nil || self.friendList!.displayName!.isEmpty {
print(
"$TAG Friend list [$nativeAddressBookFriendList] didn't exist yet, let's create it"
)
print(
"$TAG Friend list [$nativeAddressBookFriendList] didn't exist yet, let's create it"
)
self.friendList!.databaseStorageEnabled = false // We don't want to store local address-book in DB
self.friendList!.displayName = self.nativeAddressBookFriendList
self.coreContext.mCore.addFriendList(list: self.friendList!)
} else {
print(
"$TAG Friend list [$LINPHONE_ADDRESS_BOOK_FRIEND_LIST] found, removing existing friends if any"
)
} else {
print(
"$TAG Friend list [$LINPHONE_ADDRESS_BOOK_FRIEND_LIST] found, removing existing friends if any"
)
self.friendList!.friends.forEach { friend in
_ = self.friendList!.removeFriend(linphoneFriend: friend)
}
}
}
}
}
self.linphoneFriendList = self.coreContext.mCore.getFriendListByName(name: self.linphoneAddressBookFirendList)
if self.linphoneFriendList == nil {
do {
self.linphoneFriendList = try self.coreContext.mCore.createFriendList()
} catch let error {
print("Failed to enumerate contact", error)
}
}
if self.linphoneFriendList!.displayName == nil || self.linphoneFriendList!.displayName!.isEmpty {
print(
"$TAG Friend list [$linphoneAddressBookFirendList] didn't exist yet, let's create it"
)
self.linphoneFriendList!.databaseStorageEnabled = true
self.linphoneFriendList!.displayName = self.linphoneAddressBookFirendList
self.coreContext.mCore.addFriendList(list: self.linphoneFriendList!)
}
}
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
@ -87,29 +108,29 @@ final class ContactsManager: ObservableObject {
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
try store.enumerateContacts(with: request, usingBlock: { (contact, _) in
DispatchQueue.main.sync {
let newContact = Contact(
firstName: contact.givenName,
lastName: contact.familyName,
organizationName: contact.organizationName,
displayName: contact.nickname,
sipAddresses: contact.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
phoneNumbers: contact.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
imageData: ""
)
firstName: contact.givenName,
lastName: contact.familyName,
organizationName: contact.organizationName,
jobTitle: "",
displayName: contact.nickname,
sipAddresses: contact.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
phoneNumbers: contact.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
imageData: ""
)
let imageThumbnail = UIImage(data: contact.thumbnailImageData ?? Data())
self.saveImage(
image:
UIImage(data: contact.thumbnailImageData ?? Data())
image: imageThumbnail
?? self.textToImage(
firstName: contact.givenName.isEmpty
&& contact.familyName.isEmpty
&& contact.phoneNumbers.first?.value.stringValue != nil
? contact.phoneNumbers.first!.value.stringValue
: contact.givenName, lastName: contact.familyName),
name: contact.givenName + contact.familyName + String(Int.random(in: 1...1000)),
contact: newContact)
&& contact.familyName.isEmpty
&& contact.phoneNumbers.first?.value.stringValue != nil
? contact.phoneNumbers.first!.value.stringValue
: contact.givenName, lastName: contact.familyName),
name: contact.givenName + contact.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""),
contact: newContact, linphoneFriend: false, existingFriend: nil)
}
})
@ -121,9 +142,9 @@ final class ContactsManager: ObservableObject {
print("access denied")
}
}
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
}
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
}
func textToImage(firstName: String, lastName: String) -> UIImage {
let lblNameInitialize = UILabel()
@ -152,36 +173,70 @@ final class ContactsManager: ObservableObject {
return IBImgViewUserProfile
}
func saveImage(image: UIImage, name: String, contact: Contact) {
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
return
}
func saveImage(image: UIImage, name: String, contact: Contact, linphoneFriend: Bool, existingFriend: Friend?) {
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
return
}
awaitDataWrite(data: data, name: name) { _, result in
do {
let friend = try self.coreContext.mCore.createFriend()
friend.edit()
try friend.setName(newValue: contact.firstName + " " + contact.lastName)
friend.organization = contact.organizationName
let resultFriend = self.saveFriend(result: result, contact: contact, existingFriend: existingFriend)
if resultFriend != nil {
if linphoneFriend && existingFriend == nil {
_ = self.linphoneFriendList!.addLocalFriend(linphoneFriend: resultFriend!)
self.linphoneFriendList!.updateSubscriptions()
} else if existingFriend == nil {
_ = self.friendList!.addLocalFriend(linphoneFriend: resultFriend!)
self.friendList!.updateSubscriptions()
}
}
}
}
func saveFriend(result: String, contact: Contact, existingFriend: Friend?) -> Friend? {
do {
let friend = (existingFriend != nil) ? existingFriend : try self.coreContext.mCore.createFriend()
if friend != nil {
friend!.edit()
try friend!.setName(newValue: contact.firstName + " " + contact.lastName)
let friendvCard = friend!.vcard
if friendvCard != nil {
friendvCard!.givenName = contact.firstName
friendvCard!.familyName = contact.lastName
}
friend!.organization = contact.organizationName
var friendAddresses: [Address] = []
friend?.addresses.forEach({ address in
friend?.removeAddress(address: address)
})
contact.sipAddresses.forEach { sipAddress in
let address = self.coreContext.mCore.interpretUrl(url: sipAddress, applyInternationalPrefix: true)
if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) {
friend.addAddress(address: address!)
friend!.addAddress(address: address!)
friendAddresses.append(address!)
}
}
var friendPhoneNumbers: [PhoneNumber] = []
friend?.phoneNumbersWithLabel.forEach({ phoneNumber in
friend?.removePhoneNumberWithLabel(phoneNumber: phoneNumber)
})
contact.phoneNumbers.forEach { phone in
do {
if (friendPhoneNumbers.firstIndex(where: {$0.numLabel == phone.numLabel})) == nil {
let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4))
let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop)
friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber)
if (friendPhoneNumbers.firstIndex(where: {$0.num == phone.num})) == nil {
let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4))
let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop)
friend!.addPhoneNumberWithLabel(phoneNumber: phoneNumber)
friendPhoneNumbers.append(phone)
}
} catch let error {
@ -189,50 +244,61 @@ final class ContactsManager: ObservableObject {
}
}
let contactImage = result.dropFirst(8)
friend.photo = "file:/" + contactImage
friend.organization = contact.organizationName
friend!.photo = "file:/" + result
friend.done()
friend!.organization = contact.organizationName
friend!.jobTitle = contact.jobTitle
_ = self.friendList!.addLocalFriend(linphoneFriend: friend)
self.friendList!.updateSubscriptions()
} catch let error {
print("Failed to enumerate contact", error)
friend!.done()
return friend
}
} catch let error {
print("Failed to enumerate contact", error)
return nil
}
}
return nil
}
func getImagePath(friendPhotoPath: String) -> URL {
let friendPath = String(friendPhotoPath.dropFirst(6))
let imagePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(friendPath)
return imagePath
}
func awaitDataWrite(data: Data, name: String, completion: @escaping ((), String) -> Void) {
let directory = FileManager.default.temporaryDirectory
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
DispatchQueue.main.async {
if directory != nil {
DispatchQueue.main.async {
do {
let decodedData: () = try data.write(to: directory.appendingPathComponent(name + ".png"))
completion(decodedData, directory.appendingPathComponent(name + ".png").absoluteString) // <--- here, return the results
let urlName = URL(string: name)
let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : String(Int.random(in: 1...1000))
let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png"))
completion(decodedData, imagePath + ".png")
} catch {
print("Error: ", error) // need to deal with errors
completion((), "") // <--- here, should return the error
print("Error: ", error)
completion((), "")
}
}
}
}
}
struct PhoneNumber {
var numLabel: String
var num: String
var numLabel: String
var num: String
}
struct Contact: Identifiable {
var id = UUID()
var firstName: String
var lastName: String
var organizationName: String
var displayName: String
var sipAddresses: [String] = []
var phoneNumbers: [PhoneNumber] = []
var imageData: String
var id = UUID()
var firstName: String
var lastName: String
var organizationName: String
var jobTitle: String
var displayName: String
var sipAddresses: [String] = []
var phoneNumbers: [PhoneNumber] = []
var imageData: String
}

View file

@ -36,7 +36,7 @@ struct LinphoneApp: App {
AssistantView(sharedMainViewModel: sharedMainViewModel)
.toast(isShowing: $coreContext.toastMessage)
} else if coreContext.mCore.defaultAccount != nil {
ContentView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel())
ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel())
.toast(isShowing: $coreContext.toastMessage)
}
} else {

View file

@ -33,6 +33,9 @@
},
"**Contacts** : Pour vous afficher vos contacts et retrouver qui utilise Linphone." : {
},
"**Job :** %@" : {
},
"**Micro** : Pour permettre à vos correspondants de vous entendre." : {
@ -95,12 +98,18 @@
},
"Accept all" : {
},
"Add a picture" : {
},
"Add to favourites" : {
},
"All contacts" : {
},
"All modifications will be canceled." : {
},
"Appel" : {
@ -121,9 +130,6 @@
}
}
}
},
"Block" : {
},
"Block the address" : {
@ -145,6 +151,9 @@
},
"Close" : {
},
"Company" : {
},
"Conditions de service" : {
@ -190,9 +199,18 @@
},
"Domain" : {
},
"Dont save modifications?" : {
},
"Edit" : {
},
"Edit contact" : {
},
"Edit picture" : {
},
"En continuant, vous acceptez ces conditions, " : {
@ -208,6 +226,12 @@
},
"Favourites" : {
},
"First Name" : {
},
"First name*" : {
},
"History Contact fragment" : {
@ -235,6 +259,12 @@
},
"Invitation" : {
},
"Job title" : {
},
"Last name" : {
},
"Linphone" : {
@ -248,10 +278,10 @@
"Message" : {
},
"Mute" : {
"My Profile" : {
},
"My Profile" : {
"New contact" : {
},
"Next" : {
@ -297,9 +327,15 @@
},
"Personnalize your profil mode" : {
},
"Phone :" : {
},
"Phone (%@) :" : {
},
"Phone number" : {
},
"Plus tard" : {
@ -316,7 +352,10 @@
"Register" : {
},
"Remove to favourites" : {
"Remove from favourites" : {
},
"Remove picture" : {
},
"Scan QR code" : {
@ -333,6 +372,9 @@
},
"Share" : {
},
"SIP address" : {
},
"SIP address :" : {

View file

@ -23,17 +23,22 @@ struct ContactsView: View {
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@Binding var isShowEditContactFragment: Bool
@Binding var isShowDeletePopup: Bool
var body: some View {
NavigationView {
ZStack(alignment: .bottomTrailing) {
ContactsFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup)
Button {
// Action
withAnimation {
editContactViewModel.selectedEditFriend = nil
editContactViewModel.resetValues()
isShowEditContactFragment.toggle()
}
} label: {
Image("user-plus")
.padding()
@ -50,5 +55,11 @@ struct ContactsView: View {
}
#Preview {
ContactsView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel(), isShowDeletePopup: .constant(false))
ContactsView(
contactViewModel: ContactViewModel(),
historyViewModel: HistoryViewModel(),
editContactViewModel: EditContactViewModel(),
isShowEditContactFragment: .constant(false),
isShowDeletePopup: .constant(false)
)
}

View file

@ -21,21 +21,50 @@ import SwiftUI
struct ContactFragment: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@Binding var isShowDeletePopup: Bool
@Binding var isShowDismissPopup: Bool
@State private var showingSheet = false
var body: some View {
if #available(iOS 16.0, *) {
ContactInnerFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
.sheet(isPresented: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.presentationDetents([.fraction(0.2)])
}
if idiom != .pad {
ContactInnerFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
isShowDismissPopup: $isShowDismissPopup
)
.sheet(isPresented: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.presentationDetents([.fraction(0.2)])
}
} else {
ContactInnerFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
isShowDismissPopup: $isShowDismissPopup
)
.halfSheet(showSheet: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
} onDismiss: {}
}
} else {
ContactInnerFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
ContactInnerFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
isShowDismissPopup: $isShowDismissPopup
)
.halfSheet(showSheet: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
} onDismiss: {}
@ -45,5 +74,5 @@ struct ContactFragment: View {
}
#Preview {
ContactFragment(contactViewModel: ContactViewModel(), isShowDeletePopup: .constant(false))
ContactFragment(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), isShowDeletePopup: .constant(false), isShowDismissPopup: .constant(false))
}

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,8 @@ import UniformTypeIdentifiers
struct ContactListBottomSheet: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ -34,9 +36,9 @@ struct ContactListBottomSheet: View {
var body: some View {
VStack(alignment: .leading) {
if orientation == .landscapeLeft
if idiom != .pad && (orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Spacer()
HStack {
Spacer()

View file

@ -20,21 +20,30 @@
import SwiftUI
struct ContactsFragment: View {
@ObservedObject var contactViewModel: ContactViewModel
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var contactViewModel: ContactViewModel
@Binding var isShowDeletePopup: Bool
@State private var showingSheet = false
var body: some View {
@State private var showingSheet = false
var body: some View {
ZStack {
if #available(iOS 16.0, *) {
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.sheet(isPresented: $showingSheet) {
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
.presentationDetents([.fraction(0.2)])
}
if idiom != .pad {
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.sheet(isPresented: $showingSheet) {
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
.presentationDetents([.fraction(0.2)])
}
} else {
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
} onDismiss: {}
}
} else {
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
@ -42,7 +51,7 @@ struct ContactsFragment: View {
} onDismiss: {}
}
}
}
}
}
#Preview {

View file

@ -21,7 +21,9 @@ import SwiftUI
import linphonesw
struct ContactsListBottomSheet: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ -36,9 +38,9 @@ struct ContactsListBottomSheet: View {
var body: some View {
VStack(alignment: .leading) {
if orientation == .landscapeLeft
if idiom != .pad && (orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Spacer()
HStack {
Spacer()
@ -72,10 +74,10 @@ struct ContactsListBottomSheet: View {
Image(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.foregroundStyle(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true ? Color.redDanger500 : Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true
? "Remove to favourites"
? "Remove from favourites"
: "Add to favourites")
.default_text_style(styleSize: 16)
Spacer()
@ -147,10 +149,13 @@ struct ContactsListBottomSheet: View {
.background(Color.gray100)
}
.background(Color.gray100)
.frame(maxWidth: .infinity)
.onRotate { newOrientation in
orientation = newOrientation
}
.background(Color.gray100)
.frame(maxWidth: .infinity)
.onDisappear {
contactViewModel.selectedFriend = nil
}
}
}

View file

@ -64,7 +64,7 @@ struct ContactsListFragment: View {
}
if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty {
AsyncImage(url: URL(string: magicSearch.lastSearch[index].friend!.photo!)) { image in
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
@ -106,7 +106,7 @@ struct ContactsListFragment: View {
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.displayedFriend = magicSearch.lastSearch[index].friend
contactViewModel.indexDisplayedFriend = index
}
}
)

View file

@ -0,0 +1,517 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import linphonesw
struct EditContactFragment: View {
@ObservedObject var editContactViewModel: EditContactViewModel
@ObservedObject private var sharedMainViewModel = SharedMainViewModel()
@Environment(\.dismiss) var dismiss
@Binding var isShowEditContactFragment: Bool
@Binding var isShowDismissPopup: Bool
@State private var hasTimeElapsed = false
@State private var delayedColor = Color.white
@FocusState var isFirstNameFocused: Bool
@FocusState var isLastNameFocused: Bool
@FocusState var isSIPAddressFocused: Int?
@FocusState var isPhoneNumberFocused: Int?
@FocusState var isCompanyFocused: Bool
@FocusState var isJobTitleFocused: Bool
@State private var showPhotoPicker = false
@State private var selectedImage: UIImage?
@State private var removedImage = false
var body: some View {
ZStack {
VStack(spacing: 1) {
if editContactViewModel.selectedEditFriend == nil {
Rectangle()
.foregroundColor(delayedColor)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
.task(delayColor)
} else {
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(.top, 2)
.onTapGesture {
if editContactViewModel.selectedEditFriend == nil
&& editContactViewModel.firstName.isEmpty
&& editContactViewModel.lastName.isEmpty
&& editContactViewModel.sipAddresses.first!.isEmpty
&& editContactViewModel.phoneNumbers.first!.isEmpty
&& editContactViewModel.company.isEmpty
&& editContactViewModel.jobTitle.isEmpty {
delayColorDismiss()
withAnimation {
isShowEditContactFragment.toggle()
}
} else if editContactViewModel.selectedEditFriend == nil {
isShowDismissPopup.toggle()
} else {
if editContactViewModel.firstName.isEmpty
&& editContactViewModel.lastName.isEmpty
&& editContactViewModel.sipAddresses.first!.isEmpty
&& editContactViewModel.phoneNumbers.first!.isEmpty
&& editContactViewModel.company.isEmpty
&& editContactViewModel.jobTitle.isEmpty {
withAnimation {
dismiss()
}
} else {
isShowDismissPopup.toggle()
}
}
}
Text(editContactViewModel.selectedEditFriend == nil ? "New contact" : "Edit contact")
.multilineTextAlignment(.leading)
.default_text_style_orange_800(styleSize: 16)
Spacer()
Image("check")
.renderingMode(.template)
.resizable()
.foregroundStyle(editContactViewModel.firstName.isEmpty ? Color.orangeMain100 : Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.top, 2)
.disabled(editContactViewModel.firstName.isEmpty)
.onTapGesture {
withAnimation {
addOrEditFriend()
}
}
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.padding(.horizontal)
.padding(.bottom, 4)
.background(.white)
ScrollView {
VStack(spacing: 0) {
VStack(spacing: 0) {
VStack(spacing: 0) {
if editContactViewModel.selectedEditFriend != nil
&& editContactViewModel.selectedEditFriend!.photo != nil
&& !editContactViewModel.selectedEditFriend!.photo!.isEmpty && selectedImage == nil && !removedImage {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: editContactViewModel.selectedEditFriend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 100, height: 100)
case .success(let image):
image
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else if selectedImage == nil {
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Image(uiImage: selectedImage!)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
if editContactViewModel.selectedEditFriend != nil
&& editContactViewModel.selectedEditFriend!.photo != nil
&& !editContactViewModel.selectedEditFriend!.photo!.isEmpty
&& (editContactViewModel.selectedEditFriend!.photo!.suffix(11) != "default.png" || selectedImage != nil) && !removedImage {
HStack {
Spacer()
Button(action: {
showPhotoPicker = true
}, label: {
HStack {
Image("pencil-simple")
.resizable()
.frame(width: 20, height: 20)
Text("Edit picture")
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
}
})
.padding(.top, 10)
.padding(.trailing, 10)
.sheet(isPresented: $showPhotoPicker) {
PhotoPicker(filter: .images, limit: 1) { results in
PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in
if let error = errorOrNil {
print(error)
}
if let images = imagesOrNil {
if let first = images.first {
selectedImage = first
removedImage = false
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
Button(action: {
removedImage = true
selectedImage = nil
}, label: {
HStack {
Image("trash-simple")
.resizable()
.frame(width: 20, height: 20)
Text("Remove picture")
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
}
})
.padding(.top, 10)
Spacer()
}
} else {
Button(action: {
showPhotoPicker = true
}, label: {
HStack {
Image("camera")
.resizable()
.frame(width: 20, height: 20)
Text("Add a picture")
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
}
})
.padding(.top, 10)
.sheet(isPresented: $showPhotoPicker) {
PhotoPicker(filter: .images, limit: 1) { results in
PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in
if let error = errorOrNil {
print(error)
}
if let images = imagesOrNil {
if let first = images.first {
selectedImage = first
removedImage = false
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
.frame(minHeight: 150)
.frame(maxWidth: .infinity)
.padding(.top, 10)
.background(Color.gray100)
VStack(alignment: .leading) {
Text("First name*")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
TextField("First Name", text: $editContactViewModel.firstName)
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isFirstNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.padding(.bottom)
.focused($isFirstNameFocused)
}
VStack(alignment: .leading) {
Text("Last name")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
TextField("Last name", text: $editContactViewModel.lastName)
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isLastNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.padding(.bottom)
.focused($isLastNameFocused)
}
VStack(alignment: .leading) {
Text("SIP address")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
ForEach(0..<editContactViewModel.sipAddresses.count, id: \.self) { index in
HStack(alignment: .center) {
TextField("SIP address", text: $editContactViewModel.sipAddresses[index])
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isSIPAddressFocused == index ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.focused($isSIPAddressFocused, equals: index)
.onChange(of: editContactViewModel.sipAddresses[index]) { newValue in
if !newValue.isEmpty && index + 1 == editContactViewModel.sipAddresses.count {
editContactViewModel.sipAddresses.append("")
}
}
Button(action: {
editContactViewModel.sipAddresses.remove(at: index)
}, label: {
Image("x")
.renderingMode(.template)
.resizable()
.foregroundStyle(editContactViewModel.sipAddresses[index].isEmpty && editContactViewModel.sipAddresses.count == index + 1 ? Color.gray100 : Color.grayMain2c600)
.frame(width: 25, height: 25)
})
.disabled(editContactViewModel.sipAddresses[index].isEmpty && editContactViewModel.sipAddresses.count == index + 1)
.frame(maxHeight: .infinity)
}
}
}
.padding(.bottom)
VStack(alignment: .leading) {
Text("Phone number")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
ForEach(0..<editContactViewModel.phoneNumbers.count, id: \.self) { index in
HStack(alignment: .center) {
TextField("Phone number", text: $editContactViewModel.phoneNumbers[index])
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isPhoneNumberFocused == index ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.focused($isPhoneNumberFocused, equals: index)
.onChange(of: editContactViewModel.phoneNumbers[index]) { newValue in
if !newValue.isEmpty && index + 1 == editContactViewModel.phoneNumbers.count {
withAnimation {
editContactViewModel.phoneNumbers.append("")
}
}
}
Button(action: {
editContactViewModel.phoneNumbers.remove(at: index)
}, label: {
Image("x")
.renderingMode(.template)
.resizable()
.foregroundStyle(editContactViewModel.phoneNumbers[index].isEmpty && editContactViewModel.phoneNumbers.count == index + 1 ? Color.gray100 : Color.grayMain2c600)
.frame(width: 25, height: 25)
})
.disabled(editContactViewModel.phoneNumbers[index].isEmpty && editContactViewModel.phoneNumbers.count == index + 1)
.frame(maxHeight: .infinity)
}
.zIndex(isPhoneNumberFocused == index ? 1 : 0)
.transition(.move(edge: .top))
}
}
.padding(.bottom)
VStack(alignment: .leading) {
Text("Company")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
TextField("Company", text: $editContactViewModel.company)
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isCompanyFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.padding(.bottom)
.focused($isCompanyFocused)
}
VStack(alignment: .leading) {
Text("Job title")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
TextField("Job title", text: $editContactViewModel.jobTitle)
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isJobTitleFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.padding(.bottom)
.focused($isJobTitleFocused)
}
}
.frame(maxWidth: sharedMainViewModel.maxWidth)
.padding(.horizontal)
}
.frame(maxWidth: .infinity)
}
.background(Color.gray100)
}
.background(.white)
if editContactViewModel.removePopup {
ZStack {
}.onAppear {
if editContactViewModel.selectedEditFriend == nil {
delayColorDismiss()
} else {
dismiss()
}
editContactViewModel.removePopup = false
}
}
}
.navigationBarHidden(true)
}
@Sendable private func delayColor() async {
try? await Task.sleep(nanoseconds: 250_000_000)
delayedColor = Color.orangeMain500
}
func delayColorDismiss() {
if editContactViewModel.selectedEditFriend == nil {
Task {
try? await Task.sleep(nanoseconds: 80_000_000)
delayedColor = .white
}
}
}
func addOrEditFriend() {
let newContact = Contact(
firstName: editContactViewModel.firstName,
lastName: editContactViewModel.lastName,
organizationName: editContactViewModel.company,
jobTitle: editContactViewModel.jobTitle,
displayName: "",
sipAddresses: editContactViewModel.sipAddresses.map { $0 },
phoneNumbers: editContactViewModel.phoneNumbers.map { PhoneNumber(numLabel: "", num: $0)},
imageData: ""
)
if editContactViewModel.selectedEditFriend != nil && selectedImage == nil &&
!removedImage {
let saveFriendResult = ContactsManager.shared.saveFriend(
result: String(editContactViewModel.selectedEditFriend!.photo!.dropFirst(6)),
contact: newContact,
existingFriend: editContactViewModel.selectedEditFriend
)
} else {
ContactsManager.shared.saveImage(
image: selectedImage
?? ContactsManager.shared.textToImage(
firstName: editContactViewModel.firstName, lastName: editContactViewModel.lastName),
name: editContactViewModel.firstName + editContactViewModel.lastName + String(Int.random(in: 1...1000)) + ((selectedImage == nil) ? "-default" : ""),
contact: newContact, linphoneFriend: true, existingFriend: editContactViewModel.selectedEditFriend)
}
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
delayColorDismiss()
if editContactViewModel.selectedEditFriend == nil {
withAnimation {
isShowEditContactFragment.toggle()
}
} else {
dismiss()
}
editContactViewModel.resetValues()
}
}
#Preview {
EditContactFragment(editContactViewModel: EditContactViewModel(), isShowEditContactFragment: .constant(false), isShowDismissPopup: .constant(false))
}

View file

@ -32,62 +32,61 @@ struct FavoriteContactsListFragment: View {
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(0..<magicSearch.lastSearch.filter({ $0.friend?.starred == true }).count, id: \.self) { index in
Button {
} label: {
VStack {
if magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend!.photo != nil
&& !magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend!.photo!.isEmpty {
AsyncImage(
url: URL(string: magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend!.photo!)
) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 45, height: 45)
case .success(let image):
image
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
Text((magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend?.name)!)
.default_text_style(styleSize: 16)
.frame( maxWidth: .infinity, alignment: .center)
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.displayedFriend = (
magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend
)!
}
}
)
.frame(minWidth: 70, maxWidth: 70)
ForEach(0..<magicSearch.lastSearch.count, id: \.self) { index in
if magicSearch.lastSearch[index].friend != nil && magicSearch.lastSearch[index].friend!.starred == true {
Button {
} label: {
VStack {
if magicSearch.lastSearch[index].friend!.photo != nil
&& !magicSearch.lastSearch[index].friend!.photo!.isEmpty {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.photo!)
) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 45, height: 45)
case .success(let image):
image
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
Text((magicSearch.lastSearch[index].friend?.name)!)
.default_text_style(styleSize: 16)
.frame( maxWidth: .infinity, alignment: .center)
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = magicSearch.lastSearch[index].friend
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
}
)
.frame(minWidth: 70, maxWidth: 70)
}
}
}
.padding(.all, 10)

View file

@ -20,11 +20,13 @@
import linphonesw
class ContactViewModel: ObservableObject {
@Published var displayedFriend: Friend?
@Published var indexDisplayedFriend: Int?
var stringToCopy: String = ""
var selectedFriend: Friend?
var selectedFriendToDelete: Friend?
private var magicSearch = MagicSearchSingleton.shared

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import linphonesw
class EditContactViewModel: ObservableObject {
@Published var selectedEditFriend: Friend?
@Published var firstName: String = ""
@Published var lastName: String = ""
@Published var sipAddresses: [String] = []
@Published var phoneNumbers: [String] = []
@Published var company: String = ""
@Published var jobTitle: String = ""
@Published var removePopup: Bool = false
init() {
resetValues()
}
func resetValues() {
firstName = (selectedEditFriend == nil ? "" : selectedEditFriend!.vcard?.givenName) ?? ""
lastName = (selectedEditFriend == nil ? "" : selectedEditFriend!.vcard?.familyName) ?? ""
sipAddresses = []
phoneNumbers = []
company = (selectedEditFriend == nil ? "" : selectedEditFriend!.organization) ?? ""
jobTitle = (selectedEditFriend == nil ? "" : selectedEditFriend!.jobTitle) ?? ""
if selectedEditFriend != nil {
selectedEditFriend?.addresses.forEach({ address in
sipAddresses.append(String(address.asStringUriOnly().dropFirst(4)))
})
selectedEditFriend?.phoneNumbers.forEach({ phoneNumber in
phoneNumbers.append(phoneNumber)
})
}
sipAddresses.append("")
phoneNumbers.append("")
}
}

View file

@ -26,6 +26,7 @@ struct ContentView: View {
var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject private var coreContext = CoreContext.shared
@ -36,8 +37,10 @@ struct ContentView: View {
@State private var searchIsActive = false
@State private var text = ""
@FocusState private var focusedField: Bool
@State var isMenuOpen: Bool = false
@State var isMenuOpen = false
@State var isShowDeletePopup = false
@State var isShowEditContactFragment = false
@State var isShowDismissPopup = false
var body: some View {
GeometryReader { geometry in
@ -73,7 +76,7 @@ struct ContentView: View {
Button(action: {
self.index = 1
contactViewModel.displayedFriend = nil
contactViewModel.indexDisplayedFriend = nil
}, label: {
VStack {
Image("phone")
@ -273,7 +276,13 @@ struct ContentView: View {
}
if self.index == 0 {
ContactsView(contactViewModel: contactViewModel, historyViewModel: historyViewModel, isShowDeletePopup: $isShowDeletePopup)
ContactsView(
contactViewModel: contactViewModel,
historyViewModel: historyViewModel,
editContactViewModel: editContactViewModel,
isShowEditContactFragment: $isShowEditContactFragment,
isShowDeletePopup: $isShowDeletePopup
)
} else if self.index == 1 {
HistoryView()
}
@ -328,7 +337,7 @@ struct ContentView: View {
Button(action: {
self.index = 1
contactViewModel.displayedFriend = nil
contactViewModel.indexDisplayedFriend = nil
}, label: {
VStack {
Image("phone")
@ -358,7 +367,7 @@ struct ContentView: View {
}
}
if contactViewModel.displayedFriend != nil || !historyViewModel.historyTitle.isEmpty {
if contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty {
HStack(spacing: 0) {
Spacer()
.frame(maxWidth:
@ -369,7 +378,7 @@ struct ContentView: View {
: 0
)
if self.index == 0 {
ContactFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup)
ContactFragment(contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, isShowDeletePopup: $isShowDeletePopup, isShowDismissPopup: $isShowDismissPopup)
.frame(maxWidth: .infinity)
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
@ -413,31 +422,42 @@ struct ContentView: View {
.ignoresSafeArea(.all)
.zIndex(2)
if isShowEditContactFragment {
EditContactFragment(editContactViewModel: editContactViewModel, isShowEditContactFragment: $isShowEditContactFragment, isShowDismissPopup: $isShowDismissPopup)
.zIndex(3)
.transition(.move(edge: .bottom))
.onAppear {
contactViewModel.indexDisplayedFriend = nil
}
}
if isShowDeletePopup {
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeletePopup,
title: Text(
contactViewModel.selectedFriend != nil
contactViewModel.selectedFriend != nil
? "Delete \(contactViewModel.selectedFriend!.name!)?"
: (contactViewModel.displayedFriend != nil
? "Delete \(contactViewModel.displayedFriend!.name!)?"
: (contactViewModel.indexDisplayedFriend != nil
? "Delete \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?"
: "Error Name")),
content: Text("This contact will be deleted definitively."),
titleFirstButton: Text("Cancel"),
actionFirstButton: {self.isShowDeletePopup.toggle()},
actionFirstButton: {
self.isShowDeletePopup.toggle()},
titleSecondButton: Text("Ok"),
actionSecondButton: {
if contactViewModel.selectedFriend != nil {
contactViewModel.selectedFriend!.remove()
if contactViewModel.displayedFriend != nil && contactViewModel.selectedFriend!.name == contactViewModel.displayedFriend!.name {
if contactViewModel.selectedFriendToDelete != nil {
if contactViewModel.indexDisplayedFriend != nil {
withAnimation {
contactViewModel.displayedFriend = nil
contactViewModel.indexDisplayedFriend = nil
}
}
} else if contactViewModel.displayedFriend != nil {
contactViewModel.displayedFriend!.remove()
contactViewModel.selectedFriendToDelete!.remove()
} else if contactViewModel.indexDisplayedFriend != nil {
let tmpIndex = contactViewModel.indexDisplayedFriend
withAnimation {
contactViewModel.displayedFriend = nil
contactViewModel.indexDisplayedFriend = nil
}
magicSearch.lastSearch[tmpIndex!].friend!.remove()
}
magicSearch.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
@ -448,6 +468,39 @@ struct ContentView: View {
.onTapGesture {
self.isShowDeletePopup.toggle()
}
.onAppear {
contactViewModel.selectedFriendToDelete = contactViewModel.selectedFriend
}
}
if isShowDismissPopup {
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDismissPopup,
title: Text("Dont save modifications?"),
content: Text("All modifications will be canceled."),
titleFirstButton: Text("Cancel"),
actionFirstButton: {self.isShowDismissPopup.toggle()},
titleSecondButton: Text("Ok"),
actionSecondButton: {
if editContactViewModel.selectedEditFriend == nil {
self.isShowDismissPopup.toggle()
editContactViewModel.removePopup = true
editContactViewModel.resetValues()
withAnimation {
isShowEditContactFragment.toggle()
}
} else {
self.isShowDismissPopup.toggle()
editContactViewModel.resetValues()
withAnimation {
editContactViewModel.removePopup = true
}
}
})
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
self.isShowDismissPopup.toggle()
}
}
}
}
@ -462,7 +515,7 @@ struct ContentView: View {
}
}
.onRotate { newOrientation in
if (contactViewModel.displayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive {
if (contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive {
self.focusedField = false
} else if searchIsActive {
self.focusedField = true
@ -479,5 +532,5 @@ struct ContentView: View {
}
#Preview {
ContentView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel())
ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel())
}

View file

@ -47,11 +47,9 @@ class SharedMainViewModel: ObservableObject {
}
if preferences.object(forKey: displayProfileModeKey) == nil {
print("displayProfileModeKeydisplayProfileModeKey nil")
preferences.set(displayProfileMode, forKey: displayProfileModeKey)
} else {
displayProfileMode = preferences.bool(forKey: displayProfileModeKey)
print("displayProfileModeKeydisplayProfileModeKey \(displayProfileMode)")
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
let filter: PHPickerFilter
var limit: Int = 0
let onComplete: ([PHPickerResult]) -> Void
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = filter
configuration.selectionLimit = limit
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
static func convertToUIImageArray(fromResults results: [PHPickerResult], onComplete: @escaping ([UIImage]?, Error?) -> Void) {
var images = [UIImage]()
let dispatchGroup = DispatchGroup()
for result in results {
dispatchGroup.enter()
let itemProvider = result.itemProvider
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { (imageOrNil, errorOrNil) in
if let error = errorOrNil {
onComplete(nil, error)
}
if let image = imageOrNil as? UIImage {
images.append(image)
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
onComplete(images, nil)
}
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
private let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
parent.onComplete(results)
}
}
}