Merge branch 'feature/contact_create_and_edit' into 'master'

Contact create and edit

See merge request BC/private/linphone-iphone-6.0!7
This commit is contained in:
Quentin Arguillere 2023-11-14 13:53:23 +00:00
commit ae1ea15558
26 changed files with 2339 additions and 927 deletions

View file

@ -28,6 +28,7 @@
D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343332ACEFFC3009AA24E /* QRScanner.swift */; };
D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.swift */; };
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; };
D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; };
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; };
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; };
D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; };
@ -46,6 +47,11 @@
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 */; };
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF32AFA66F900D938CB /* EditContactController.swift */; };
D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.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 */; };
@ -88,6 +94,7 @@
D72343332ACEFFC3009AA24E /* QRScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanner.swift; sourceTree = "<group>"; };
D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = "<group>"; };
D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = "<group>"; };
D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = "<group>"; };
D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = "<group>"; };
D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = "<group>"; };
@ -107,6 +114,11 @@
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>"; };
D7C48DF32AFA66F900D938CB /* EditContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactController.swift; sourceTree = "<group>"; };
D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerActionsFragment.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>"; };
@ -150,6 +162,9 @@
D717071F2AC5989C0037746F /* TextExtension.swift */,
D74C9D002ACB098C0021626A /* PermissionManager.swift */,
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */,
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
D7C48DF32AFA66F900D938CB /* EditContactController.swift */,
D732A9082AFD235500DB42BA /* ShareSheetController.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -315,6 +330,8 @@
D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */,
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */,
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */,
D7C365092AF001C300FE6142 /* EditContactFragment.swift */,
D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */,
);
path = Fragments;
sourceTree = "<group>";
@ -325,6 +342,7 @@
D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */,
D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */,
D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */,
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -484,21 +502,25 @@
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 */,
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 */,
@ -513,8 +535,10 @@
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */,
D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */,
D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */,
D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */,
D72343362AD037AF009AA24E /* ToastView.swift in Sources */,
D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */,
D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */,
D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */,
D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */,
D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.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

@ -20,62 +20,72 @@
import linphonesw
import Contacts
import SwiftUI
import ContactsUI
final class ContactsManager: ObservableObject {
static let shared = ContactsManager()
final class 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()
@Published var friendList: FriendList?
private init() {
fetchContacts()
}
func fetchContacts() {
private var coreContext = CoreContext.shared
private var magicSearch = MagicSearchSingleton.shared
private let nativeAddressBookFriendList = "Native address-book"
let linphoneAddressBookFriendList = "Linphone address-book"
var friendList: FriendList?
var linphoneFriendList: FriendList?
private init() {
fetchContacts()
}
func fetchContacts() {
coreContext.doOnCoreQueue { core in
if core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off {
print("$TAG Core is being stopped or already destroyed, abort")
print("\(#function) - Core is being stopped or already destroyed, abort")
} else {
print("$TAG ${friends.size} friends created")
self.friendList = core.getFriendListByName(name: self.nativeAddressBookFriendList)
if self.friendList == nil {
do {
self.friendList = try core.createFriendList()
} 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"
)
self.friendList!.databaseStorageEnabled = false // We don't want to store local address-book in DB
self.friendList!.displayName = self.nativeAddressBookFriendList
core.addFriendList(list: self.friendList!)
} 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)
}
}
}
do {
self.friendList = try core.getFriendListByName(name: self.nativeAddressBookFriendList) ?? core.createFriendList()
} catch let error {
print("\(#function) - Failed to enumerate contacts: \(error)")
}
if let friendList = self.friendList {
if friendList.displayName == nil || friendList.displayName!.isEmpty {
print("\(#function) - Friend list '\(self.nativeAddressBookFriendList)' didn't exist yet, let's create it")
friendList.databaseStorageEnabled = false // We don't want to store local address-book in DB
friendList.displayName = self.nativeAddressBookFriendList
core.addFriendList(list: friendList)
} else {
print("\(#function) - Friend list '\(friendList.displayName!) found, removing existing friends if any")
friendList.friends.forEach { friend in
_ = friendList.removeFriend(linphoneFriend: friend)
}
}
}
do {
self.linphoneFriendList = try core.getFriendListByName(name: self.linphoneAddressBookFriendList) ?? core.createFriendList()
} catch let error {
print("\(#function) - Failed to enumerate contacts: \(error)")
}
if let linphoneFriendList = self.linphoneFriendList {
if linphoneFriendList.displayName == nil || linphoneFriendList.displayName!.isEmpty {
print("\(#function) - Friend list \(self.linphoneAddressBookFriendList) didn't exist yet, let's create it")
linphoneFriendList.databaseStorageEnabled = true
linphoneFriendList.displayName = self.linphoneAddressBookFriendList
core.addFriendList(list: linphoneFriendList)
}
}
}
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let error = error {
print("failed to request access", error)
print("\(#function) - failed to request access", error)
return
}
if granted {
@ -83,47 +93,48 @@ final class ContactsManager: ObservableObject {
CNContactFamilyNameKey, CNContactGivenNameKey, CNContactNicknameKey,
CNContactPostalAddressesKey, CNContactIdentifierKey,
CNInstantMessageAddressUsernameKey, CNContactInstantMessageAddressesKey,
CNContactImageDataKey, CNContactThumbnailImageDataKey, CNContactOrganizationNameKey]
CNContactOrganizationNameKey, CNContactImageDataAvailableKey, CNContactImageDataKey, CNContactThumbnailImageDataKey]
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: ""
)
identifier: contact.identifier,
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)
}
})
} catch let error {
print("Failed to enumerate contact", error)
print("\(#function) - Failed to enumerate contact", error)
}
} else {
print("access denied")
print("\(#function) - 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,93 +163,138 @@ 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
self.coreContext.doOnCoreQueue() { core in
do {
var friend = try core.createFriend()
friend.edit()
try friend.setName(newValue: contact.firstName + " " + contact.lastName)
friend.organization = contact.organizationName
var friendAddresses: [Address] = []
contact.sipAddresses.forEach { sipAddress in
let address = core.interpretUrl(url: sipAddress, applyInternationalPrefix: true)
if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) {
friend.addAddress(address: address!)
friendAddresses.append(address!)
}
self.saveFriend(result: result, contact: contact, existingFriend: existingFriend) { resultFriend in
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()
}
var friendPhoneNumbers: [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)
friendPhoneNumbers.append(phone)
}
} catch let error {
print("Failed to enumerate contact", error)
}
}
let contactImage = result.dropFirst(8)
friend.photo = "file:/" + contactImage
friend.organization = contact.organizationName
friend.done()
DispatchQueue.main.async {
_ = self.friendList!.addLocalFriend(linphoneFriend: friend)
self.friendList!.updateSubscriptions()
}
} catch let error {
print("Failed to enumerate contact", error)
}
}
}
}
func awaitDataWrite(data: Data, name: String, completion: @escaping ((), String) -> Void) {
let directory = FileManager.default.temporaryDirectory
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
} catch {
print("Error: ", error) // need to deal with errors
completion((), "") // <--- here, should return the error
func saveFriend(result: String, contact: Contact, existingFriend: Friend?, completion: @escaping (Friend?) -> Void) {
self.coreContext.doOnCoreQueue { core in
do {
let friend = try existingFriend ?? core.createFriend()
friend.edit()
friend.nativeUri = contact.identifier
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 = core.interpretUrl(url: sipAddress, applyInternationalPrefix: true)
if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) {
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.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 {
print("\(#function) - Failed to create friend phone number for \(phone.numLabel):", error)
}
}
friend.photo = "file:/" + result
friend.organization = contact.organizationName
friend.jobTitle = contact.jobTitle
friend.done()
completion(friend)
} catch let error {
print("Failed to enumerate contact", error)
completion(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.urls(for: .documentDirectory, in: .userDomainMask).first
if directory != nil {
DispatchQueue.main.async {
do {
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)
completion((), "")
}
}
}
}
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 {
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 identifier: String
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 @@ final class CoreContext: ObservableObject {
private init() {}
func doOnCoreQueue(synchronous : Bool = false, lambda: @escaping (Core) -> Void) {
func doOnCoreQueue(synchronous: Bool = false, lambda: @escaping (Core) -> Void) {
if synchronous {
coreQueue.sync {
lambda(self.mCore)
@ -77,7 +77,9 @@ final class CoreContext: ObservableObject {
}
}
self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnMainQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in
self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnMainQueue {(cbVal:
(core: Core, account: Account, state: RegistrationState, message: String)
) in
// If account has been configured correctly, we will go through Progress and Ok states
// Otherwise, we will be Failed.
NSLog("New registration state is \(cbVal.state) for user id " +

View file

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

View file

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

View file

@ -52,7 +52,14 @@ class AccountLoginViewModel: ObservableObject {
// userID is set to null as it's the same as the username in our case
// ha1 is set to null as we are using the clear text password. Upon first register, the hash will be computed automatically.
// The realm will be determined automatically from the first register, as well as the algorithm
let authInfo = try Factory.Instance.createAuthInfo(username: self.username, userid: "", passwd: self.passwd, ha1: "", realm: "", domain: self.domain)
let authInfo = try Factory.Instance.createAuthInfo(
username: self.username,
userid: "",
passwd: self.passwd,
ha1: "",
realm: "",
domain: self.domain
)
// Account object replaces deprecated ProxyConfig object
// Account object is configured through an AccountParams object that we can obtain from the Core

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

@ -18,32 +18,87 @@
*/
import SwiftUI
import Contacts
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
@State private var showShareSheet = false
var body: some View {
if #available(iOS 16.0, *) {
ContactInnerFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
if idiom != .pad {
ContactInnerFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
cnContact: CNContact(),
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDismissPopup: $isShowDismissPopup
)
.sheet(isPresented: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.presentationDetents([.fraction(0.2)])
}
} else {
ContactInnerFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!)
.presentationDetents([.medium])
.edgesIgnoringSafeArea(.bottom)
}
} else {
ContactInnerFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
cnContact: CNContact(),
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDismissPopup: $isShowDismissPopup
)
.halfSheet(showSheet: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
} onDismiss: {}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!)
.edgesIgnoringSafeArea(.bottom)
}
}
} else {
ContactInnerFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
cnContact: CNContact(),
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDismissPopup: $isShowDismissPopup
)
.halfSheet(showSheet: $showingSheet) {
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
} onDismiss: {}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!)
.edgesIgnoringSafeArea(.bottom)
}
}
}
}
#Preview {
ContactFragment(contactViewModel: ContactViewModel(), isShowDeletePopup: .constant(false))
ContactFragment(
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
isShowDeletePopup: .constant(false),
isShowDismissPopup: .constant(false)
)
}

View file

@ -0,0 +1,442 @@
//
// ContactInnerActionsFragment.swift
// Linphone
//
// Created by Benoît Martins on 09/11/2023.
//
import SwiftUI
struct ContactInnerActionsFragment: View {
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@State private var informationIsOpen = true
@Binding var showingSheet: Bool
@Binding var showShareSheet: Bool
@Binding var isShowDeletePopup: Bool
@Binding var isShowDismissPopup: Bool
var actionEditButton: () -> Void
var body: some View {
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.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
ForEach(0..<magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count, id: \.self) { index in
Button {
} label: {
HStack {
VStack {
Text("SIP address :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly().dropFirst(4))
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy = magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
}
}
)
if !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.isEmpty
|| index < magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count - 1 {
VStack {
Divider()
}
.padding(.horizontal)
}
}
ForEach(0..<magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count, id: \.self) { index in
Button {
} label: {
HStack {
VStack {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label!.isEmpty {
Text("Phone (\(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label!)) :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Text("Phone :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
}
Text(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy =
magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
}
}
)
if index < magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count - 1 {
VStack {
Divider()
}
.padding(.horizontal)
}
}
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
if contactViewModel.indexDisplayedFriend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& ((magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty)
|| (magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!.isEmpty)) {
VStack {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty {
Text("**Company :** \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!)")
.default_text_style(styleSize: 14)
.padding(.vertical, 15)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!.isEmpty {
Text("**Job :** \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!)")
.default_text_style(styleSize: 14)
.padding(.top,
magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty
? 0 : 15
)
.padding(.bottom, 15)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.background(.white)
.cornerRadius(15)
.padding(.top)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
// TODO Trust Fragment
// TODO Medias Fragment
HStack(alignment: .center) {
Text("Other actions")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
VStack(spacing: 0) {
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < magicSearch.lastSearch.count
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
Button {
actionEditButton()
} label: {
HStack {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Edit")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
} else {
NavigationLink(destination: EditContactFragment(
editContactViewModel: editContactViewModel,
isShowEditContactFragment: .constant(false),
isShowDismissPopup: $isShowDismissPopup)) {
HStack {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Edit")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.simultaneousGesture(
TapGesture().onEnded {
editContactViewModel.selectedEditFriend = magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend
editContactViewModel.resetValues()
}
)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
contactViewModel.objectWillChange.send()
magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred.toggle()
}
} label: {
HStack {
Image(contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.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
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true
? "Remove from favourites"
: "Add to favourites")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
showShareSheet.toggle()
} label: {
HStack {
Image("share-network")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Share")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
/*
Button {
} label: {
HStack {
Image("bell-simple-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Mute")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
} label: {
HStack {
Image("x-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Block")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
*/
Button {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
isShowDeletePopup.toggle()
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25)
Text("Delete this contact")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
}
#Preview {
ContactInnerActionsFragment(
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
showingSheet: .constant(false),
showShareSheet: .constant(false),
isShowDeletePopup: .constant(false),
isShowDismissPopup: .constant(false),
actionEditButton: {}
)
}

View file

@ -18,244 +18,159 @@
*/
import SwiftUI
import Contacts
import ContactsUI
struct ContactInnerFragment: View {
@ObservedObject private var sharedMainViewModel = SharedMainViewModel()
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@State private var orientation = UIDevice.current.orientation
@State private var informationIsOpen = true
@State private var presentingEditContact = false
@State var cnContact: CNContact?
@Binding var isShowDeletePopup: Bool
@Binding var showingSheet: Bool
@Binding var showShareSheet: 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()
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < magicSearch.lastSearch.count
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
Button(action: {
editNativeContact()
}, 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)
.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 && contactViewModel.indexDisplayedFriend! < magicSearch.lastSearch.count
&& 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()
.aspectRatio(contentMode: .fill)
.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..<contactViewModel.displayedFriend!.addresses.count, id: \.self) { index in
Button {
} label: {
HStack {
VStack {
Text("SIP address :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(contactViewModel.displayedFriend!.addresses[index].asStringUriOnly().dropFirst(4))
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Button(action: {
}, label: {
VStack {
HStack(alignment: .center) {
Image("phone")
.renderingMode(.template)
.resizable()
@ -267,50 +182,23 @@ struct ContactInnerFragment: View {
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("Appel")
.default_text_style(styleSize: 14)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy = contactViewModel.displayedFriend!.addresses[index].asStringUriOnly()
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
}
}
)
if !contactViewModel.displayedFriend!.phoneNumbers.isEmpty || index < contactViewModel.displayedFriend!.addresses.count - 1 {
VStack {
Divider()
}
.padding(.horizontal)
}
}
})
ForEach(0..<contactViewModel.displayedFriend!.phoneNumbers.count, id: \.self) { index in
Button {
} label: {
HStack {
VStack {
Text("Phone (\(contactViewModel.displayedFriend!.phoneNumbersWithLabel[index].label!)) :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(contactViewModel.displayedFriend!.phoneNumbersWithLabel[index].phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
Spacer()
Button(action: {
}, label: {
VStack {
HStack(alignment: .center) {
Image("chat-teardrop-text")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
@ -321,245 +209,106 @@ struct ContactInnerFragment: View {
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("Message")
.default_text_style(styleSize: 14)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy = contactViewModel.displayedFriend!.phoneNumbersWithLabel[index].phoneNumber
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
}
}
)
})
Spacer()
Button(action: {
if index < contactViewModel.displayedFriend!.phoneNumbers.count - 1 {
VStack {
Divider()
}, label: {
VStack {
HStack(alignment: .center) {
Image("video-camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(.horizontal)
.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)
ContactInnerActionsFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDeletePopup: $isShowDeletePopup,
isShowDismissPopup: $isShowDismissPopup,
actionEditButton: editNativeContact
)
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
.frame(maxWidth: sharedMainViewModel.maxWidth)
}
if contactViewModel.displayedFriend != nil
&& contactViewModel.displayedFriend!.organization != nil
&& !contactViewModel.displayedFriend!.organization!.isEmpty {
VStack {
Text("**Company :** \(contactViewModel.displayedFriend!.organization!)")
.default_text_style(styleSize: 14)
.padding(.vertical, 15)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
.background(.white)
.cornerRadius(15)
.padding(.top)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
// TODO Trust Fragment
// TODO Medias Fragment
HStack(alignment: .center) {
Text("Other actions")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
VStack(spacing: 0) {
Button {
} label: {
HStack {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Edit")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
if contactViewModel.displayedFriend != nil {
contactViewModel.objectWillChange.send()
contactViewModel.displayedFriend!.starred.toggle()
}
} label: {
HStack {
Image(contactViewModel.displayedFriend != nil && contactViewModel.displayedFriend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text(contactViewModel.displayedFriend != nil && contactViewModel.displayedFriend!.starred == true
? "Remove to favourites"
: "Add to favourites")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
} label: {
HStack {
Image("share-network")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Share")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
} label: {
HStack {
Image("bell-simple-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Mute")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
} label: {
HStack {
Image("x-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("Block")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
if contactViewModel.displayedFriend != nil {
isShowDeletePopup.toggle()
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25)
Text("Delete this contact")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
.frame(maxWidth: .infinity)
}
.background(Color.gray100)
}
.background(.white)
.navigationBarHidden(true)
.onRotate { newOrientation in
orientation = newOrientation
}
.fullScreenCover(isPresented: $presentingEditContact) {
NavigationView {
EditContactView(contact: $cnContact)
.navigationBarTitle("Edit Contact")
.navigationBarTitleDisplayMode(.inline)
.edgesIgnoringSafeArea(.vertical)
}
}
.background(Color.gray100)
}
.background(.white)
.navigationBarHidden(true)
.onRotate { newOrientation in
orientation = newOrientation
.navigationViewStyle(.stack)
}
func editNativeContact() {
do {
let store = CNContactStore()
let descriptor = CNContactViewController.descriptorForRequiredKeys()
cnContact = try store.unifiedContact(
withIdentifier: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!,
keysToFetch: [descriptor]
)
if cnContact != nil {
presentingEditContact.toggle()
}
} catch {
print(error)
}
}
}
#Preview {
ContactInnerFragment(contactViewModel: ContactViewModel(), isShowDeletePopup: .constant(false), showingSheet: .constant(false))
ContactInnerFragment(
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
isShowDeletePopup: .constant(false),
showingSheet: .constant(false),
showShareSheet: .constant(false),
isShowDismissPopup: .constant(false)
)
}

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()
@ -91,7 +93,12 @@ struct ContactListBottomSheet: View {
if contactViewModel.stringToCopy.prefix(4) != "sip:" {
Button {
if #available(iOS 16.0, *) {
showingSheet.toggle()
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()

View file

@ -20,29 +20,67 @@
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
@State private var showShareSheet = 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,
showShareSheet: $showShareSheet
)
.presentationDetents([.fraction(0.2)])
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!)
.presentationDetents([.medium])
.edgesIgnoringSafeArea(.bottom)
}
} else {
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
ContactsListBottomSheet(
contactViewModel: contactViewModel,
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
showShareSheet: $showShareSheet
)
} onDismiss: {}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!)
.edgesIgnoringSafeArea(.bottom)
}
}
} else {
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
ContactsListBottomSheet(
contactViewModel: contactViewModel,
isShowDeletePopup: $isShowDeletePopup,
showingSheet: $showingSheet,
showShareSheet: $showShareSheet
)
} onDismiss: {}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!)
.edgesIgnoringSafeArea(.bottom)
}
}
}
}
}
}
#Preview {

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

@ -19,138 +19,169 @@
import SwiftUI
import linphonesw
import Contacts
struct ContactsListBottomSheet: View {
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@State private var orientation = UIDevice.current.orientation
@Environment(\.dismiss) var dismiss
@Environment(\.dismiss) var dismiss
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@State private var orientation = UIDevice.current.orientation
@Binding var isShowDeletePopup: Bool
@Binding var showingSheet: Bool
var body: some View {
VStack(alignment: .leading) {
if orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
Spacer()
HStack {
Spacer()
Button("Close") {
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
}
}
.padding(.trailing)
}
Spacer()
Button {
if contactViewModel.selectedFriend != nil {
contactViewModel.selectedFriend!.starred.toggle()
}
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
@Binding var showingSheet: Bool
@Binding var showShareSheet: Bool
var body: some View {
VStack(alignment: .leading) {
if idiom != .pad && (orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Spacer()
HStack {
Spacer()
Button("Close") {
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
}
}
.padding(.trailing)
}
Spacer()
Button {
if contactViewModel.selectedFriend != nil {
contactViewModel.selectedFriend!.starred.toggle()
}
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true
? "Remove to favourites"
: "Add to favourites")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("share-network")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Share")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if contactViewModel.selectedFriend != nil {
isShowDeletePopup.toggle()
}
.renderingMode(.template)
.resizable()
.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 from favourites"
: "Add to favourites")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
}
.onRotate { newOrientation in
orientation = newOrientation
}
.background(Color.gray100)
.frame(maxWidth: .infinity)
}
contactViewModel.selectedFriendToShare = contactViewModel.selectedFriend
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
showShareSheet.toggle()
}
} label: {
HStack {
Image("share-network")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Share")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if contactViewModel.selectedFriend != nil {
isShowDeletePopup.toggle()
}
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
}
.background(Color.gray100)
.frame(maxWidth: .infinity)
.onRotate { newOrientation in
orientation = newOrientation
}
.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()
@ -72,6 +72,7 @@ struct ContactsListFragment: View {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
@ -106,7 +107,7 @@ struct ContactsListFragment: View {
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.displayedFriend = magicSearch.lastSearch[index].friend
contactViewModel.indexDisplayedFriend = index
}
}
)

View file

@ -0,0 +1,531 @@
/*
* 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()
.aspectRatio(contentMode: .fill)
.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()
.aspectRatio(contentMode: .fill)
.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(
identifier: editContactViewModel.identifier,
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 {
ContactsManager.shared.saveFriend(
result: String(editContactViewModel.selectedEditFriend!.photo!.dropFirst(6)),
contact: newContact,
existingFriend: editContactViewModel.selectedEditFriend, completion: {_ in }
)
} 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,62 @@ 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()
.aspectRatio(contentMode: .fill)
.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,14 @@
import linphonesw
class ContactViewModel: ObservableObject {
@Published var displayedFriend: Friend?
@Published var indexDisplayedFriend: Int?
var stringToCopy: String = ""
var selectedFriend: Friend?
var selectedFriendToShare: Friend?
var selectedFriendToDelete: Friend?
private var magicSearch = MagicSearchSingleton.shared

View file

@ -0,0 +1,62 @@
/*
* 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 identifier: String = ""
@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() {
identifier = (selectedEditFriend == nil ? "" : selectedEditFriend!.nativeUri) ?? ""
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,12 @@ 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 +427,46 @@ 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
? "Delete \(contactViewModel.selectedFriend!.name!)?"
: (contactViewModel.displayedFriend != nil
? "Delete \(contactViewModel.displayedFriend!.name!)?"
: "Error Name")),
contactViewModel.selectedFriend != nil
? "Delete \(contactViewModel.selectedFriend!.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 +477,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 +524,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 +541,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,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>) {
}
}

View file

@ -39,7 +39,7 @@ final class MagicSearchSingleton: ObservableObject {
private var domainDefaultAccount = ""
private init() {
coreContext.doOnCoreQueue{ core in
coreContext.doOnCoreQueue { core in
self.domainDefaultAccount = core.defaultAccount?.params?.domain ?? ""
self.magicSearch = try? core.createMagicSearch()
@ -53,7 +53,7 @@ final class MagicSearchSingleton: ObservableObject {
}
func searchForContacts(sourceFlags: Int) {
coreContext.doOnCoreQueue{ core in
coreContext.doOnCoreQueue { _ in
var needResetCache = false
DispatchQueue.main.sync {

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

View file

@ -0,0 +1,91 @@
/*
* 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 Foundation
import SwiftUI
import linphonesw
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let friendToShare: Friend
var activityItems: [Any] = []
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
if directoryURL != nil {
if friendToShare.name != nil {
let filename = friendToShare.name!.replacingOccurrences(of: " ", with: "")
let fileURL = directoryURL!
.appendingPathComponent(filename)
.appendingPathExtension("vcf")
if friendToShare.vcard != nil {
try? friendToShare.vcard!.asVcard4String().write(to: fileURL, atomically: false, encoding: String.Encoding.utf8)
let controller = UIActivityViewController(
activityItems: [fileURL],
applicationActivities: applicationActivities
)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
}
}
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
// nothing to do here
}
func shareContacts(friend: String) {
let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
if directoryURL != nil {
let filename = NSUUID().uuidString
let fileURL = directoryURL!
.appendingPathComponent(filename)
.appendingPathExtension("vcf")
try? friend.write(to: fileURL, atomically: false, encoding: String.Encoding.utf8)
}
/*
let activityViewController = UIActivityViewController(
activityItems: [fileURL],
applicationActivities: nil
)
*/
}
}