Add edit view for native contact

This commit is contained in:
Benoit Martins 2023-11-07 17:02:03 +01:00
parent abd5461f54
commit 5d0ce2c8f3
9 changed files with 280 additions and 75 deletions

View file

@ -50,6 +50,7 @@
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 */; };
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF32AFA66F900D938CB /* EditContactController.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 */; };
@ -116,6 +117,7 @@
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>"; };
D7C48DF32AFA66F900D938CB /* EditContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactController.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>"; };
@ -172,6 +174,7 @@
D74C9D002ACB098C0021626A /* PermissionManager.swift */,
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */,
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
D7C48DF32AFA66F900D938CB /* EditContactController.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -568,6 +571,7 @@
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */,
D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */,
D7E6D04D2AEBD77600A57AAF /* CustomBottomSheet.swift in Sources */,
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */,
D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */,
D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */,
D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */,

View file

@ -20,6 +20,7 @@
import linphonesw
import Contacts
import SwiftUI
import ContactsUI
final class ContactsManager: ObservableObject {
@ -110,6 +111,7 @@ final class ContactsManager: ObservableObject {
try store.enumerateContacts(with: request, usingBlock: { (contact, _) in
DispatchQueue.main.sync {
let newContact = Contact(
identifier: contact.identifier,
firstName: contact.givenName,
lastName: contact.familyName,
organizationName: contact.organizationName,
@ -203,6 +205,8 @@ final class ContactsManager: ObservableObject {
if friend != nil {
friend!.edit()
friend!.nativeUri = contact.identifier
try friend!.setName(newValue: contact.firstName + " " + contact.lastName)
let friendvCard = friend!.vcard
@ -284,6 +288,55 @@ final class ContactsManager: ObservableObject {
}
}
}
func getCNContact(friend: Friend, completion: @escaping (CNContact?) -> Void) {
DispatchQueue.global().async {
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let error = error {
print("failed to request access", error)
return
}
if granted {
let keys = [CNContactEmailAddressesKey, CNContactPhoneNumbersKey,
CNContactFamilyNameKey, CNContactGivenNameKey, CNContactNicknameKey,
CNContactPostalAddressesKey, CNContactIdentifierKey,
CNInstantMessageAddressUsernameKey, CNContactInstantMessageAddressesKey,
CNContactImageDataKey, CNContactThumbnailImageDataKey, CNContactOrganizationNameKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
try store.enumerateContacts(with: request, usingBlock: { (contact, _) in
if contact.identifier == friend.nativeUri {
var contactFetched = contact
if !contactFetched.areKeysAvailable([CNContactViewController.descriptorForRequiredKeys()]) {
do {
contactFetched = try store.unifiedContact(withIdentifier: contact.identifier, keysToFetch: [CNContactViewController.descriptorForRequiredKeys()])
completion(contactFetched)
}
catch {
completion(nil)
}
}
}
})
} catch let error {
print("Failed to enumerate contact", error)
}
} else {
print("access denied")
}
}
}
}
func getFriend(contact: Contact) -> Friend? {
if friendList != nil {
let friend = friendList!.friends.first(where: {$0.nativeUri == contact.identifier})
return friend
} else {
return nil
}
}
}
struct PhoneNumber {
@ -293,6 +346,7 @@ struct PhoneNumber {
struct Contact: Identifiable {
var id = UUID()
var identifier: String
var firstName: String
var lastName: String
var organizationName: String

View file

@ -208,6 +208,9 @@
},
"Edit contact" : {
},
"Edit Contact" : {
},
"Edit picture" : {

View file

@ -74,5 +74,10 @@ struct ContactFragment: View {
}
#Preview {
ContactFragment(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), isShowDeletePopup: .constant(false), isShowDismissPopup: .constant(false))
ContactFragment(
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
isShowDeletePopup: .constant(false),
isShowDismissPopup: .constant(false)
)
}

View file

@ -18,6 +18,7 @@
*/
import SwiftUI
import Contacts
struct ContactInnerFragment: View {
@ -30,6 +31,8 @@ struct ContactInnerFragment: View {
@State private var orientation = UIDevice.current.orientation
@State private var informationIsOpen = true
@State private var presentingEditContact = false
@State private var cnContact: CNContact?
@Binding var isShowDeletePopup: Bool
@Binding var showingSheet: Bool
@ -44,8 +47,7 @@ struct ContactInnerFragment: View {
.frame(height: 0)
HStack {
if !(orientation == .landscapeLeft
|| orientation == .landscapeRight
if !(orientation == .landscapeLeft || orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Image("caret-left")
.renderingMode(.template)
@ -61,21 +63,40 @@ struct ContactInnerFragment: View {
}
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()
if contactViewModel.indexDisplayedFriend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil && !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
Button(action: {
ContactsManager.shared.getCNContact(friend: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) { result in
cnContact = result
if cnContact != nil {
presentingEditContact.toggle()
}
}
}, label: {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.top, 2)
})
} else {
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)
@ -143,7 +164,6 @@ struct ContactInnerFragment: View {
Spacer()
Button(action: {
}, label: {
VStack {
HStack(alignment: .center) {
@ -458,8 +478,7 @@ struct ContactInnerFragment: View {
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactViewModel.indexDisplayedFriend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
.foregroundStyle(contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? Color.redDanger500 : Color.grayMain2c500)
.frame(width: 25, height: 25)
Text(contactViewModel.indexDisplayedFriend != nil
@ -600,6 +619,14 @@ struct ContactInnerFragment: View {
.onRotate { newOrientation in
orientation = newOrientation
}
.sheet(isPresented: $presentingEditContact) {
NavigationView {
AnyView(EditContactView(contact: $cnContact)
.navigationBarTitle("Edit Contact")
.navigationBarTitleDisplayMode(.inline))
.edgesIgnoringSafeArea(.top)
}
}
}
.navigationViewStyle(.stack)
}

View file

@ -18,64 +18,77 @@
*/
import SwiftUI
import linphonesw
struct ContactsInnerFragment: View {
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@State private var isFavoriteOpen = true
@Binding var showingSheet: Bool
var body: some View {
VStack(alignment: .leading) {
if !magicSearch.lastSearch.filter({ $0.friend?.starred == true }).isEmpty {
HStack(alignment: .center) {
Text("Favourites")
.default_text_style_800(styleSize: 16)
Spacer()
Image(isFavoriteOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
}
.padding(.top, 30)
.padding(.horizontal, 16)
.background(.white)
.onTapGesture {
withAnimation {
isFavoriteOpen.toggle()
}
}
if isFavoriteOpen {
FavoriteContactsListFragment(
contactViewModel: contactViewModel,
favoriteContactsListViewModel: FavoriteContactsListViewModel(),
showingSheet: $showingSheet)
.zIndex(-1)
.transition(.move(edge: .top))
}
HStack(alignment: .center) {
Text("All contacts")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.top, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)
}
.navigationBarHidden(true)
}
@Environment(\.scenePhase) var scenePhase
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@State private var isFavoriteOpen = true
@Binding var showingSheet: Bool
var body: some View {
VStack(alignment: .leading) {
if !magicSearch.lastSearch.filter({ $0.friend?.starred == true }).isEmpty {
HStack(alignment: .center) {
Text("Favourites")
.default_text_style_800(styleSize: 16)
Spacer()
Image(isFavoriteOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
}
.padding(.top, 30)
.padding(.horizontal, 16)
.background(.white)
.onTapGesture {
withAnimation {
isFavoriteOpen.toggle()
}
}
if isFavoriteOpen {
FavoriteContactsListFragment(
contactViewModel: contactViewModel,
favoriteContactsListViewModel: FavoriteContactsListViewModel(),
showingSheet: $showingSheet)
.zIndex(-1)
.transition(.move(edge: .top))
}
HStack(alignment: .center) {
Text("All contacts")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.top, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)
}
.navigationBarHidden(true)
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
ContactsManager.shared.fetchContacts()
print("Active")
} else if newPhase == .inactive {
print("Inactive")
} else if newPhase == .background {
print("Background")
}
}
}
}
#Preview {
ContactsInnerFragment(contactViewModel: ContactViewModel(), showingSheet: .constant(false))
ContactsInnerFragment(contactViewModel: ContactViewModel(), showingSheet: .constant(false))
}

View file

@ -472,6 +472,7 @@ struct EditContactFragment: View {
func addOrEditFriend() {
let newContact = Contact(
identifier: editContactViewModel.identifier,
firstName: editContactViewModel.firstName,
lastName: editContactViewModel.lastName,
organizationName: editContactViewModel.company,

View file

@ -23,6 +23,7 @@ class EditContactViewModel: ObservableObject {
@Published var selectedEditFriend: Friend?
@Published var identifier: String = ""
@Published var firstName: String = ""
@Published var lastName: String = ""
@Published var sipAddresses: [String] = []
@ -36,6 +37,7 @@ class EditContactViewModel: ObservableObject {
}
func resetValues() {
identifier = (selectedEditFriend == nil ? "" : selectedEditFriend!.nativeUri) ?? ""
firstName = (selectedEditFriend == nil ? "" : selectedEditFriend!.vcard?.givenName) ?? ""
lastName = (selectedEditFriend == nil ? "" : selectedEditFriend!.vcard?.familyName) ?? ""
sipAddresses = []

View file

@ -0,0 +1,96 @@
/*
* 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 ContactsUI
import linphonesw
struct EditContactView: UIViewControllerRepresentable {
class Coordinator: NSObject, CNContactViewControllerDelegate, UINavigationControllerDelegate {
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
if let cnc = contact {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
self.parent.contact = cnc
let newContact = Contact(
identifier: cnc.identifier,
firstName: cnc.givenName,
lastName: cnc.familyName,
organizationName: cnc.organizationName,
jobTitle: "",
displayName: cnc.nickname,
sipAddresses: cnc.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
phoneNumbers: cnc.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
imageData: ""
)
let imageThumbnail = UIImage(data: contact!.thumbnailImageData ?? Data())
ContactsManager.shared.saveImage(
image: imageThumbnail
?? ContactsManager.shared.textToImage(
firstName: cnc.givenName.isEmpty
&& cnc.familyName.isEmpty
&& cnc.phoneNumbers.first?.value.stringValue != nil
? cnc.phoneNumbers.first!.value.stringValue
: cnc.givenName, lastName: cnc.familyName),
name: cnc.givenName + cnc.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""),
contact: newContact,
linphoneFriend: false,
existingFriend: ContactsManager.shared.getFriend(contact: newContact))
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
}
viewController.dismiss(animated: true, completion: {})
}
func contactViewController(_ viewController: CNContactViewController, shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
return true
}
var parent: EditContactView
init(_ parent: EditContactView) {
self.parent = parent
}
}
@Binding var contact: CNContact?
init(contact: Binding<CNContact?>) {
self._contact = contact
}
typealias UIViewControllerType = CNContactViewController
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<EditContactView>) -> EditContactView.UIViewControllerType {
let vcontact = contact != nil ? CNContactViewController(for: contact!) : CNContactViewController(forNewContact: CNContact())
vcontact.isEditing = true
vcontact.delegate = context.coordinator
return vcontact
}
func updateUIViewController(_ uiViewController: EditContactView.UIViewControllerType, context: UIViewControllerRepresentableContext<EditContactView>) {
}
}