diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 5e3cbc295..0dfea3d76 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -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 = ""; }; D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = ""; }; D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = ""; }; + D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = ""; }; + D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactViewModel.swift; sourceTree = ""; }; + D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = ""; }; D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = ""; }; D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = ""; }; @@ -165,6 +171,7 @@ D717071F2AC5989C0037746F /* TextExtension.swift */, D74C9D002ACB098C0021626A /* PermissionManager.swift */, D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */, + D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, ); path = Utils; sourceTree = ""; @@ -331,6 +338,7 @@ D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */, D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */, D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */, + D7C365092AF001C300FE6142 /* EditContactFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -341,6 +349,7 @@ D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */, D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */, D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */, + D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -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 */, diff --git a/Linphone/Assets.xcassets/profil-picture-default.imageset/profil-picture-default.svg b/Linphone/Assets.xcassets/profil-picture-default.imageset/profil-picture-default.svg index b2bea3b29..557a5087a 100644 --- a/Linphone/Assets.xcassets/profil-picture-default.imageset/profil-picture-default.svg +++ b/Linphone/Assets.xcassets/profil-picture-default.imageset/profil-picture-default.svg @@ -1,10 +1,10 @@ - + - - + + - + diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 1e31e8716..76eafa748 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -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 } diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index b507a0c87..5aa85a39b 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -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 { diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 79915ed4f..0a4d62d83 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -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" : { + }, + "Don’t 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 :" : { diff --git a/Linphone/UI/Main/Contacts/ContactsView.swift b/Linphone/UI/Main/Contacts/ContactsView.swift index 4d00f8bdb..a8191f047 100644 --- a/Linphone/UI/Main/Contacts/ContactsView.swift +++ b/Linphone/UI/Main/Contacts/ContactsView.swift @@ -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) + ) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index 042169629..abbd1492c 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -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)) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index dc31e9c35..63ccd2937 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -21,241 +21,132 @@ import SwiftUI struct ContactInnerFragment: View { + @ObservedObject private var sharedMainViewModel = SharedMainViewModel() + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var magicSearch = MagicSearchSingleton.shared @State private var orientation = UIDevice.current.orientation @State private var informationIsOpen = true @Binding var isShowDeletePopup: Bool - @Binding var showingSheet: Bool + @Binding var isShowDismissPopup: Bool var body: some View { - VStack(spacing: 1) { - Rectangle() - .foregroundColor(Color.orangeMain500) - .edgesIgnoringSafeArea(.top) - .frame(height: 0) - - HStack { - if !(orientation == .landscapeLeft - || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { - Image("caret-left") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.top, 2) - .onTapGesture { - withAnimation { - contactViewModel.displayedFriend = nil + NavigationView { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + if !(orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) + .onTapGesture { + withAnimation { + contactViewModel.indexDisplayedFriend = nil + } } - } - } - - Spacer() - - Image("pencil-simple") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.top, 2) - .onTapGesture { - withAnimation { - - } } - } - .frame(maxWidth: .infinity) - .frame(height: 50) - .padding(.horizontal) - .padding(.bottom, 4) - .background(.white) - - ScrollView { - VStack(spacing: 0) { + + Spacer() + + NavigationLink(destination: EditContactFragment(editContactViewModel: editContactViewModel, isShowEditContactFragment: .constant(false), isShowDismissPopup: $isShowDismissPopup)) { + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) + } + .simultaneousGesture( + TapGesture().onEnded { + editContactViewModel.selectedEditFriend = magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend + editContactViewModel.resetValues() + } + ) + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { VStack(spacing: 0) { - if contactViewModel.displayedFriend != nil - && contactViewModel.displayedFriend!.photo != nil - && !contactViewModel.displayedFriend!.photo!.isEmpty { - AsyncImage(url: URL(string: contactViewModel.displayedFriend!.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: + VStack(spacing: 0) { + VStack(spacing: 0) { + if contactViewModel.indexDisplayedFriend != nil + && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil + && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo != nil + && !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo!.isEmpty { + AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.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 contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { Image("profil-picture-default") .resizable() .frame(width: 100, height: 100) .clipShape(Circle()) - @unknown default: - EmptyView() } - } - } else if contactViewModel.displayedFriend != nil { - Image("profil-picture-default") - .resizable() - .frame(width: 100, height: 100) - .clipShape(Circle()) - } - if contactViewModel.displayedFriend != nil && contactViewModel.displayedFriend?.name != nil { - Text((contactViewModel.displayedFriend?.name)!) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 10) - - Text("En ligne") - .foregroundStyle(Color.greenSuccess500) - .multilineTextAlignment(.center) - .default_text_style_300(styleSize: 12) - .frame(maxWidth: .infinity) - } - - } - .frame(minHeight: 150) - .frame(maxWidth: .infinity) - .padding(.top, 10) - .background(Color.gray100) - - HStack { - Spacer() - - Button(action: { - - }, label: { - VStack { - HStack(alignment: .center) { - Image("phone") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25) - .onTapGesture { - withAnimation { - - } - } + if contactViewModel.indexDisplayedFriend != nil + && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil + && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend?.name != nil { + Text((magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend?.name)!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text("En ligne") + .foregroundStyle(Color.greenSuccess500) + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) } - .padding(16) - .background(Color.grayMain2c200) - .cornerRadius(40) - Text("Appel") - .default_text_style(styleSize: 14) } - }) - - Spacer() - - Button(action: { + .frame(minHeight: 150) + .frame(maxWidth: .infinity) + .padding(.top, 10) + .background(Color.gray100) - }, label: { - VStack { - HStack(alignment: .center) { - Image("chat-teardrop-text") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25) - .onTapGesture { - withAnimation { - - } - } - } - .padding(16) - .background(Color.grayMain2c200) - .cornerRadius(40) + HStack { + Spacer() - Text("Message") - .default_text_style(styleSize: 14) - } - }) - - Spacer() - - Button(action: { - - }, label: { - VStack { - HStack(alignment: .center) { - Image("video-camera") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25) - .onTapGesture { - withAnimation { - - } - } - } - .padding(16) - .background(Color.grayMain2c200) - .cornerRadius(40) - - Text("Video Call") - .default_text_style(styleSize: 14) - } - }) - - Spacer() - } - .padding(.top, 20) - .frame(maxWidth: .infinity) - .background(Color.gray100) - - HStack(alignment: .center) { - Text("Information") - .default_text_style_800(styleSize: 16) - - Spacer() - - Image(informationIsOpen ? "caret-up" : "caret-down") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) - } - .padding(.top, 30) - .padding(.bottom, 10) - .padding(.horizontal, 16) - .background(Color.gray100) - .onTapGesture { - withAnimation { - informationIsOpen.toggle() - } - } - - if informationIsOpen { - VStack(spacing: 0) { - if contactViewModel.displayedFriend != nil { - ForEach(0.. UIScreen.main.bounds.size.height { + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { Spacer() HStack { Spacer() diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift index 9196f23cc..e875bd037 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift @@ -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 { diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift index a5d27c278..8e65df8a8 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift @@ -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 + } } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 355f67d0f..b4696b326 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -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 } } ) diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift new file mode 100644 index 000000000..a3259fc84 --- /dev/null +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -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 . + */ + +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... + */ + +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("") + } +} diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 2be1c0356..3f60aa239 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -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("Don’t 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()) } diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index d95e98499..6adcc0dcc 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -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)") } } diff --git a/Linphone/Utils/PhotoPicker.swift b/Linphone/Utils/PhotoPicker.swift new file mode 100644 index 000000000..8e4c951aa --- /dev/null +++ b/Linphone/Utils/PhotoPicker.swift @@ -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 . + */ + +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) + } + } +}