From 9ef96bbd780aef4eae03d13e4741af9a12f6bbfc Mon Sep 17 00:00:00 2001 From: "benoit.martins" Date: Thu, 19 Oct 2023 17:25:50 +0200 Subject: [PATCH] Add contacts list --- Linphone.xcodeproj/project.pbxproj | 114 ++-- .../xcshareddata/xcschemes/Linphone.xcscheme | 77 +++ .../caret-up.imageset/Contents.json | 21 + .../caret-up.imageset/caret-up.svg | 1 + .../check.imageset/Contents.json | 21 + .../Assets.xcassets/check.imageset/check.svg | 1 + .../green-check.imageset/Contents.json | 21 + .../green-check.imageset/green-check.svg | 3 + .../Assets.xcassets/x.imageset/Contents.json | 21 + Linphone/Assets.xcassets/x.imageset/x.svg | 1 + Linphone/Contacts/ContactsManager.swift | 250 +++++++ Linphone/Core/CoreContext.swift | 3 + Linphone/LinphoneApp.swift | 2 +- Linphone/Localizable.xcstrings | 15 +- .../Fragments/PermissionsFragment.swift | 1 + Linphone/UI/Main/Contacts/ContactsView.swift | 52 +- .../Contacts/Fragments/ContactFragment.swift | 88 ++- .../Contacts/Fragments/ContactsFragment.swift | 80 +++ .../Fragments/ContactsListFragment.swift | 115 ++++ .../FavoriteContactsListFragment.swift | 80 +++ .../Contacts/ViewModel/ContactViewModel.swift | 6 +- .../ViewModel/ContactsListViewModel.swift | 31 + .../FavoriteContactsListViewModel.swift | 25 + Linphone/UI/Main/ContentView.swift | 637 +++++++++++------- Linphone/Utils/MagicSearchSingleton.swift | 70 ++ Linphone/Utils/PermissionManager.swift | 11 + Linphone/Utils/TextExtension.swift | 5 + 27 files changed, 1376 insertions(+), 376 deletions(-) create mode 100644 Linphone.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme create mode 100644 Linphone/Assets.xcassets/caret-up.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/caret-up.imageset/caret-up.svg create mode 100644 Linphone/Assets.xcassets/check.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/check.imageset/check.svg create mode 100644 Linphone/Assets.xcassets/green-check.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/green-check.imageset/green-check.svg create mode 100644 Linphone/Assets.xcassets/x.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/x.imageset/x.svg create mode 100644 Linphone/Contacts/ContactsManager.swift create mode 100644 Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift create mode 100644 Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift create mode 100644 Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift create mode 100644 Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift create mode 100644 Linphone/UI/Main/Contacts/ViewModel/FavoriteContactsListViewModel.swift create mode 100644 Linphone/Utils/MagicSearchSingleton.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 3e27fc401..1ab50b742 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 886500223A8E518D3EE5FCB7 /* Pods_Linphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A334B8FDAD2893691A734BE /* Pods_Linphone.framework */; }; D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */; }; D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */; }; D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071D2AC5922E0037746F /* ColorExtension.swift */; }; @@ -18,6 +19,9 @@ D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABC82ABC6FD700B41C10 /* CoreContext.swift */; }; D719ABCC2ABC769C00B41C10 /* AssistantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABCB2ABC769C00B41C10 /* AssistantView.swift */; }; D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABCE2ABC779A00B41C10 /* AccountLoginViewModel.swift */; }; + D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */; }; + D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */; }; + D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */; }; D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72250622ADE9615008FB426 /* HistoryViewModel.swift */; }; D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72250682ADFBF2D008FB426 /* SideMenu.swift */; }; D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723432F2ACEFEF8009AA24E /* QrCodeScannerFragment.swift */; }; @@ -34,12 +38,14 @@ D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9D002ACB098C0021626A /* PermissionManager.swift */; }; D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */; }; D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7702EF12AC7205000557C00 /* WelcomeView.swift */; }; - D78290B82ADD3910004AA85C /* ContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290B72ADD3910004AA85C /* ContactFragment.swift */; }; + D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; }; + D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290B72ADD3910004AA85C /* ContactsFragment.swift */; }; D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */; }; D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */; }; D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FC52ACC458A0081A588 /* SplashScreen.swift */; }; D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.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 */; }; D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */; }; @@ -48,14 +54,15 @@ D7D24D182AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D122AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf */; }; D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */; }; D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; }; + D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; }; + D7E6D04B2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */; }; D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */; }; D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */; }; - F4BB8DFBA0FF08430EBA9351 /* Pods_Linphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F5B27C5576B1EAED2F205EB /* Pods_Linphone.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 377E0B5C2B1F38192E694334 /* Pods-Linphone.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Linphone.debug.xcconfig"; path = "Target Support Files/Pods-Linphone/Pods-Linphone.debug.xcconfig"; sourceTree = ""; }; - 6F5B27C5576B1EAED2F205EB /* Pods_Linphone.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Linphone.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1A334B8FDAD2893691A734BE /* Pods_Linphone.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Linphone.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1DE4CD5FD6E1F01639F27E3B /* Pods-Linphone.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Linphone.release.xcconfig"; path = "Target Support Files/Pods-Linphone/Pods-Linphone.release.xcconfig"; sourceTree = ""; }; D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; D717071D2AC5922E0037746F /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; @@ -69,6 +76,9 @@ D719ABC82ABC6FD700B41C10 /* CoreContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreContext.swift; sourceTree = ""; }; D719ABCB2ABC769C00B41C10 /* AssistantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantView.swift; sourceTree = ""; }; D719ABCE2ABC779A00B41C10 /* AccountLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLoginViewModel.swift; sourceTree = ""; }; + D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListViewModel.swift; sourceTree = ""; }; + D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListFragment.swift; sourceTree = ""; }; + D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactFragment.swift; sourceTree = ""; }; D72250622ADE9615008FB426 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = ""; }; D72250682ADFBF2D008FB426 /* SideMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenu.swift; sourceTree = ""; }; D723432F2ACEFEF8009AA24E /* QrCodeScannerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeScannerFragment.swift; sourceTree = ""; }; @@ -85,13 +95,15 @@ D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = ""; }; D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; - D78290B72ADD3910004AA85C /* ContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactFragment.swift; sourceTree = ""; }; + D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; + D78290B72ADD3910004AA85C /* ContactsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsFragment.swift; sourceTree = ""; }; D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; D7A03FC52ACC458A0081A588 /* SplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedMainViewModel.swift; sourceTree = ""; }; D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = ""; }; D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = ""; }; D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = ""; }; D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Light.ttf"; sourceTree = ""; }; @@ -100,9 +112,11 @@ D7D24D122AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-ExtraBold.ttf"; sourceTree = ""; }; D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFragment.swift; sourceTree = ""; }; D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = ""; }; + D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = ""; }; + D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListViewModel.swift; sourceTree = ""; }; D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsFragment.swift; sourceTree = ""; }; D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterFragment.swift; sourceTree = ""; }; - F76FB87556A3109F61F9E2D5 /* Pods-Linphone.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Linphone.release.xcconfig"; path = "Target Support Files/Pods-Linphone/Pods-Linphone.release.xcconfig"; sourceTree = ""; }; + FB718F405DAF7B9993AEB878 /* Pods-Linphone.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Linphone.debug.xcconfig"; path = "Target Support Files/Pods-Linphone/Pods-Linphone.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -110,17 +124,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F4BB8DFBA0FF08430EBA9351 /* Pods_Linphone.framework in Frameworks */, + 886500223A8E518D3EE5FCB7 /* Pods_Linphone.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 52EFCC310713B3CA01062945 /* Frameworks */ = { + 1CD95087B17CAD149119B7C2 /* Frameworks */ = { isa = PBXGroup; children = ( - 6F5B27C5576B1EAED2F205EB /* Pods_Linphone.framework */, + 1A334B8FDAD2893691A734BE /* Pods_Linphone.framework */, ); name = Frameworks; sourceTree = ""; @@ -128,8 +142,8 @@ A31AF2AB8C6A3D7B7EA3B424 /* Pods */ = { isa = PBXGroup; children = ( - 377E0B5C2B1F38192E694334 /* Pods-Linphone.debug.xcconfig */, - F76FB87556A3109F61F9E2D5 /* Pods-Linphone.release.xcconfig */, + FB718F405DAF7B9993AEB878 /* Pods-Linphone.debug.xcconfig */, + 1DE4CD5FD6E1F01639F27E3B /* Pods-Linphone.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -140,6 +154,7 @@ D717071D2AC5922E0037746F /* ColorExtension.swift */, D717071F2AC5989C0037746F /* TextExtension.swift */, D74C9D002ACB098C0021626A /* PermissionManager.swift */, + D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */, ); path = Utils; sourceTree = ""; @@ -150,7 +165,7 @@ D719ABB52ABC67BF00B41C10 /* Linphone */, D719ABB42ABC67BF00B41C10 /* Products */, A31AF2AB8C6A3D7B7EA3B424 /* Pods */, - 52EFCC310713B3CA01062945 /* Frameworks */, + 1CD95087B17CAD149119B7C2 /* Frameworks */, ); sourceTree = ""; }; @@ -167,6 +182,7 @@ children = ( D7A03FC52ACC458A0081A588 /* SplashScreen.swift */, D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */, + D777DBB12AE12C4000565A99 /* Contacts */, D719ABC72ABC6FB200B41C10 /* Core */, D719ABC52ABC6EE800B41C10 /* UI */, D717071C2AC591EF0037746F /* Utils */, @@ -285,10 +301,21 @@ path = Welcome; sourceTree = ""; }; + D777DBB12AE12C4000565A99 /* Contacts */ = { + isa = PBXGroup; + children = ( + D777DBB22AE12C5900565A99 /* ContactsManager.swift */, + ); + path = Contacts; + sourceTree = ""; + }; D78290B62ADD38F9004AA85C /* Fragments */ = { isa = PBXGroup; children = ( - D78290B72ADD3910004AA85C /* ContactFragment.swift */, + D78290B72ADD3910004AA85C /* ContactsFragment.swift */, + D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */, + D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */, + D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -297,6 +324,8 @@ isa = PBXGroup; children = ( D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */, + D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */, + D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -363,12 +392,12 @@ isa = PBXNativeTarget; buildConfigurationList = D719ABC22ABC67BF00B41C10 /* Build configuration list for PBXNativeTarget "Linphone" */; buildPhases = ( - 6FE8573A5CFC1DA89D3172B5 /* [CP] Check Pods Manifest.lock */, + BE9432280D0A11AA770A50FD /* [CP] Check Pods Manifest.lock */, D719ABAF2ABC67BF00B41C10 /* Sources */, D719ABB02ABC67BF00B41C10 /* Frameworks */, D719ABB12ABC67BF00B41C10 /* Resources */, D7FB55122AD53FE200A5AB15 /* Run Script */, - 230129DD87A6EBB04DF458AD /* [CP] Embed Pods Frameworks */, + D5CA1ECD620857DB91E334A5 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -432,24 +461,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 230129DD87A6EBB04DF458AD /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Linphone/Pods-Linphone-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Linphone/Pods-Linphone-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Linphone/Pods-Linphone-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 6FE8573A5CFC1DA89D3172B5 /* [CP] Check Pods Manifest.lock */ = { + BE9432280D0A11AA770A50FD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -471,6 +483,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + D5CA1ECD620857DB91E334A5 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Linphone/Pods-Linphone-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Linphone/Pods-Linphone-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Linphone/Pods-Linphone-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; D7FB55122AD53FE200A5AB15 /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -499,16 +528,21 @@ files = ( D71707202AC5989C0037746F /* TextExtension.swift in Sources */, D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */, + D71FCA832AE14D6E00D2E43E /* ContactFragment.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 */, D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, + D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, + D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */, D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, @@ -522,12 +556,14 @@ D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */, D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */, + D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */, - D78290B82ADD3910004AA85C /* ContactFragment.swift in Sources */, + D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */, D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */, D74C9CFC2ACACF370021626A /* WelcomePage3Fragment.swift in Sources */, D719ABCC2ABC769C00B41C10 /* AssistantView.swift in Sources */, + D7E6D04B2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift in Sources */, D74C9CFA2ACACF2D0021626A /* WelcomePage2Fragment.swift in Sources */, D74C9CFF2ACAEC5E0021626A /* PopupView.swift in Sources */, D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */, @@ -652,7 +688,7 @@ }; D719ABC32ABC67BF00B41C10 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 377E0B5C2B1F38192E694334 /* Pods-Linphone.debug.xcconfig */; + baseConfigurationReference = FB718F405DAF7B9993AEB878 /* Pods-Linphone.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -668,7 +704,8 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Share photos with your friends and customize avatars"; - INFOPLIST_KEY_NSPhotoLibraryUsageDescription = ""; + INFOPLIST_KEY_NSContactsUsageDescription = "Make calls with your friends"; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Share photos with your friends and customize avatars"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -696,7 +733,7 @@ }; D719ABC42ABC67BF00B41C10 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F76FB87556A3109F61F9E2D5 /* Pods-Linphone.release.xcconfig */; + baseConfigurationReference = 1DE4CD5FD6E1F01639F27E3B /* Pods-Linphone.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -712,7 +749,8 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Share photos with your friends and customize avatars"; - INFOPLIST_KEY_NSPhotoLibraryUsageDescription = ""; + INFOPLIST_KEY_NSContactsUsageDescription = "Make calls with your friends"; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Share photos with your friends and customize avatars"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; diff --git a/Linphone.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme b/Linphone.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme new file mode 100644 index 000000000..f5986b566 --- /dev/null +++ b/Linphone.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Linphone/Assets.xcassets/caret-up.imageset/Contents.json b/Linphone/Assets.xcassets/caret-up.imageset/Contents.json new file mode 100644 index 000000000..c7fea1d89 --- /dev/null +++ b/Linphone/Assets.xcassets/caret-up.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "caret-up.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/caret-up.imageset/caret-up.svg b/Linphone/Assets.xcassets/caret-up.imageset/caret-up.svg new file mode 100644 index 000000000..dacc592b1 --- /dev/null +++ b/Linphone/Assets.xcassets/caret-up.imageset/caret-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/check.imageset/Contents.json b/Linphone/Assets.xcassets/check.imageset/Contents.json new file mode 100644 index 000000000..17203ccbd --- /dev/null +++ b/Linphone/Assets.xcassets/check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "check.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/check.imageset/check.svg b/Linphone/Assets.xcassets/check.imageset/check.svg new file mode 100644 index 000000000..a8d374215 --- /dev/null +++ b/Linphone/Assets.xcassets/check.imageset/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/green-check.imageset/Contents.json b/Linphone/Assets.xcassets/green-check.imageset/Contents.json new file mode 100644 index 000000000..f4e39fa87 --- /dev/null +++ b/Linphone/Assets.xcassets/green-check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "green-check.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/green-check.imageset/green-check.svg b/Linphone/Assets.xcassets/green-check.imageset/green-check.svg new file mode 100644 index 000000000..1d42925ba --- /dev/null +++ b/Linphone/Assets.xcassets/green-check.imageset/green-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/Assets.xcassets/x.imageset/Contents.json b/Linphone/Assets.xcassets/x.imageset/Contents.json new file mode 100644 index 000000000..74ec74c05 --- /dev/null +++ b/Linphone/Assets.xcassets/x.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "x.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/x.imageset/x.svg b/Linphone/Assets.xcassets/x.imageset/x.svg new file mode 100644 index 000000000..707720548 --- /dev/null +++ b/Linphone/Assets.xcassets/x.imageset/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift new file mode 100644 index 000000000..5de3eb683 --- /dev/null +++ b/Linphone/Contacts/ContactsManager.swift @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of Linphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import linphonesw +import Contacts +import SwiftUI + +final class ContactsManager: ObservableObject { + + static let shared = ContactsManager() + + private var coreContext = CoreContext.shared + + @Published var contacts: [Contact] = [] + + private let nativeAddressBookFriendList = "Native address-book" + let linphoneAddressBookFirendList = "Linphone address-book" + + private init() { + fetchContacts() + } + + func fetchContacts() { + contacts.removeAll() + DispatchQueue.global().async { + let store = CNContactStore() + store.requestAccess(for: .contacts) { (granted, error) in + if let error = error { + print("failed to request access", error) + return + } + if granted { + let keys = [CNContactEmailAddressesKey, CNContactPhoneNumbersKey, + CNContactFamilyNameKey, CNContactGivenNameKey, CNContactNicknameKey, + CNContactPostalAddressesKey, CNContactIdentifierKey, + CNInstantMessageAddressUsernameKey, CNContactInstantMessageAddressesKey, + CNContactImageDataKey, CNContactThumbnailImageDataKey, CNContactOrganizationNameKey] + let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor]) + do { + try store.enumerateContacts(with: request, usingBlock: { (contact, _) in + + DispatchQueue.main.sync { + self.contacts.append( + 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: self.saveImage( + image: + UIImage(data: contact.thumbnailImageData ?? Data()) + ?? 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.identifier) + ) + ) + } + self.contacts.sort(by: { + $0.firstName.folding( + options: .diacriticInsensitive, locale: .current + ) < $1.firstName.folding( + options: .diacriticInsensitive, locale: .current + ) + }) + }) + + } catch let error { + print("Failed to enumerate contact", error) + } + + } else { + print("access denied") + } + } + + var friends: [Friend] = [] + + self.contacts.forEach { contact in + do { + let friend = try self.coreContext.mCore.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 = self.coreContext.mCore.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] = [] + contact.phoneNumbers.forEach { phone in + do { + if (friendPhoneNumbers.firstIndex(where: {$0.numLabel == phone.numLabel})) == nil { + let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: phone.numLabel) + friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber) + friendPhoneNumbers.append(phone) + } + } catch let error { + print("Failed to enumerate contact", error) + } + } + + let contactImage = contact.imageData.dropFirst(8) + friend.photo = "file:/" + contactImage + + friend.done() + friends.append(friend) + + } catch let error { + print("Failed to enumerate contact", error) + } + } + + if self.coreContext.mCore.globalState == GlobalState.Shutdown || self.coreContext.mCore.globalState == GlobalState.Off { + print("$TAG Core is being stopped or already destroyed, abort") + } else if friends.isEmpty { + print("$TAG No friend created!") + } else { + print("$TAG ${friends.size} friends created") + + let fetchedFriends = friends + + let nativeFriendList = self.coreContext.mCore.getFriendListByName(name: self.nativeAddressBookFriendList) + var friendList = nativeFriendList + if friendList == nil { + do { + friendList = try self.coreContext.mCore.createFriendList() + } catch let error { + print("Failed to enumerate contact", error) + } + } + + if friendList!.displayName == nil || friendList!.displayName!.isEmpty { + print( + "$TAG Friend list [$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 + self.coreContext.mCore.addFriendList(list: friendList!) + } else { + print( + "$TAG Friend list [$LINPHONE_ADDRESS_BOOK_FRIEND_LIST] found, removing existing friends if any" + ) + friendList!.friends.forEach { friend in + _ = friendList!.removeFriend(linphoneFriend: friend) + } + } + + fetchedFriends.forEach { friend in + _ = friendList!.addLocalFriend(linphoneFriend: friend) + } + + friends.removeAll() + + print("$TAG Friends added") + + friendList!.updateSubscriptions() + print("$TAG Subscription(s) updated") + } + } + } + + func saveImage(image: UIImage, name: String) -> String { + guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else { + return "" + } + let directory = FileManager.default.temporaryDirectory + print("FileManagerFileManager \(directory.absoluteString)") + do { + try data.write(to: directory.appendingPathComponent(name + ".png")) + return directory.appendingPathComponent(name + ".png").absoluteString + } catch { + print(error.localizedDescription) + return "" + } + } + + func textToImage(firstName: String, lastName: String) -> UIImage { + + let lblNameInitialize = UILabel() + lblNameInitialize.frame.size = CGSize(width: 100.0, height: 100.0) + lblNameInitialize.font = UIFont(name: "NotoSans-ExtraBold", size: 40) + lblNameInitialize.textColor = UIColor(Color.grayMain2c600) + + var textToDisplay = "" + if firstName.first != nil { + textToDisplay += String(firstName.first!) + } + if lastName.first != nil { + textToDisplay += String(lastName.first!) + } + + lblNameInitialize.text = textToDisplay.uppercased() + lblNameInitialize.textAlignment = .center + lblNameInitialize.backgroundColor = UIColor(Color.grayMain2c200) + lblNameInitialize.layer.cornerRadius = 10.0 + + var IBImgViewUserProfile = UIImage() + UIGraphicsBeginImageContext(lblNameInitialize.frame.size) + lblNameInitialize.layer.render(in: UIGraphicsGetCurrentContext()!) + IBImgViewUserProfile = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return IBImgViewUserProfile + } +} + +struct PhoneNumber { + 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 +} diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index f20316b7f..560cb3cbc 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -40,6 +40,9 @@ final class CoreContext: ObservableObject { let factory = Factory.Instance let configDir = factory.getConfigDir(context: nil) try? mCore = Factory.Instance.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) + + mCore.friendsDatabasePath = "\(configDir)/friends.db" + try? mCore.start() // Create a Core listener to listen for the callback we need diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 3dda9cf64..b6eba8007 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -28,7 +28,7 @@ struct LinphoneApp: App { var body: some Scene { WindowGroup { if isActive { - ContentView(sharedMainViewModel: SharedMainViewModel(), contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel()) + ContentView(sharedMainViewModel: SharedMainViewModel(), contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel()) .toast(isShowing: $coreContext.toastMessage) } else { SplashScreen(isActive: $isActive) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 8bd88f0a8..28e74c979 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -36,9 +36,6 @@ }, "**Notifications** : Pour vous informé quand vous recevez un message ou un appel." : { - }, - "%lld" : { - }, "%lld Book (Example)" : { "extractionState" : "manual", @@ -95,6 +92,9 @@ }, "Accept all" : { + }, + "All contacts" : { + }, "assistant_account_login" : { "extractionState" : "manual", @@ -160,6 +160,9 @@ }, "Error" : { + }, + "Favourites" : { + }, "History Contact fragment" : { @@ -252,6 +255,12 @@ }, "Sécurisé" : { + }, + "See all" : { + + }, + "See Linphone contact" : { + }, "sip.linphone.org" : { diff --git a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift index cbb56aa76..979428da3 100644 --- a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift +++ b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift @@ -192,6 +192,7 @@ struct PermissionsFragment: View { .padding(.horizontal) Button { + permissionManager.contactsRequestPermission() permissionManager.cameraRequestPermission() } label: { Text("D'accord") diff --git a/Linphone/UI/Main/Contacts/ContactsView.swift b/Linphone/UI/Main/Contacts/ContactsView.swift index cecad9f2a..74098595a 100644 --- a/Linphone/UI/Main/Contacts/ContactsView.swift +++ b/Linphone/UI/Main/Contacts/ContactsView.swift @@ -23,58 +23,12 @@ struct ContactsView: View { @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var historyViewModel: HistoryViewModel - - @State private var orientation = UIDevice.current.orientation - @State private var selectedIndex = 0 - - var objects: [Int] = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39 - ] - + var body: some View { NavigationView { ZStack(alignment: .bottomTrailing) { - VStack(spacing: 0) { - VStack { - List { - ForEach(objects, id: \.self) { index in - Button { - withAnimation { - contactViewModel.contactTitle = String(index) - } - } label: { - Text("\(index)") - .frame( maxWidth: .infinity, alignment: .leading) - .foregroundStyle(Color.orangeMain500) - } - .buttonStyle(.borderless) - .listRowSeparator(.hidden) - } - } - .listStyle(.plain) - .overlay( - VStack { - if objects.isEmpty { - Spacer() - Image("illus-belledonne1") - .resizable() - .scaledToFit() - .clipped() - .padding(.all) - Text("No contacts for the moment...") - .default_text_style_800(styleSize: 16) - Spacer() - Spacer() - } - } - .padding(.all) - ) - } - } - .onRotate { newOrientation in - orientation = newOrientation - } + + ContactsFragment(contactViewModel: contactViewModel) Button { // Action diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index 9168b860e..c3684a141 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -20,58 +20,50 @@ import SwiftUI struct ContactFragment: View { - - @ObservedObject var contactViewModel: ContactViewModel - - @State private var orientation = UIDevice.current.orientation - + + @ObservedObject var contactViewModel: ContactViewModel + + @State private var orientation = UIDevice.current.orientation + var body: some View { - VStack(alignment: .leading) { - - if !(orientation == .landscapeLeft || orientation == .landscapeRight || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { - HStack { - Image("caret-left") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.top, 20) - .onTapGesture { - withAnimation { - contactViewModel.contactTitle = "" - } - } - - Spacer() - } - .padding(.leading) - } - - Spacer() - - Text(contactViewModel.contactTitle) - .frame(maxWidth: .infinity) - - List { - ForEach(1...40, id: \.self) { index in - Button { - contactViewModel.contactTitle = String(index) - } label: { - Text("\(index)") - .frame( maxWidth: .infinity, alignment: .leading) - } - .buttonStyle(.borderless) - } - } - } - .navigationBarHidden(true) - .onRotate { newOrientation in - orientation = newOrientation - } + VStack(alignment: .leading) { + + if !(orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 20) + .onTapGesture { + withAnimation { + contactViewModel.contactTitle = "" + } + } + + Spacer() + } + .padding(.leading) + } + + Spacer() + + Text("Contact Fragment " + contactViewModel.contactTitle) + .frame(maxWidth: .infinity) + + Spacer() + } + .navigationBarHidden(true) + .onRotate { newOrientation in + orientation = newOrientation + } } } #Preview { - ContactFragment(contactViewModel: ContactViewModel()) + ContactFragment(contactViewModel: ContactViewModel()) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift new file mode 100644 index 000000000..00e11cc3a --- /dev/null +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI + +struct ContactsFragment: View { + + @ObservedObject var contactViewModel: ContactViewModel + + @State private var orientation = UIDevice.current.orientation + + @State var isFavoriteOpen: Bool = true + + var body: some View { + VStack(alignment: .leading) { + 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()) + .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()) + } + .navigationBarHidden(true) + .onRotate { newOrientation in + orientation = newOrientation + } + + } +} + +#Preview { + ContactsFragment(contactViewModel: ContactViewModel()) +} diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift new file mode 100644 index 000000000..cd1c984a1 --- /dev/null +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI +import linphonesw + +struct ContactsListFragment: View { + + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var contactsListViewModel: ContactsListViewModel + + var body: some View { + VStack { + List { + ForEach(0... + */ + +import SwiftUI + +struct FavoriteContactsListFragment: View { + + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var favoriteContactsListViewModel: FavoriteContactsListViewModel + + var body: some View { + ScrollView(.horizontal) { + HStack { + ForEach(0... */ -import Foundation +import linphonesw class ContactViewModel: ObservableObject { - - @Published var contactTitle: String = "" + + @Published var contactTitle: String = "" init() {} } diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift new file mode 100644 index 000000000..565f9a6c4 --- /dev/null +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import linphonesw + +class ContactsListViewModel: ObservableObject { + + private var magicSearch = MagicSearchSingleton.shared + private var coreContext = CoreContext.shared + + init() { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } +} diff --git a/Linphone/UI/Main/Contacts/ViewModel/FavoriteContactsListViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/FavoriteContactsListViewModel.swift new file mode 100644 index 000000000..852da7946 --- /dev/null +++ b/Linphone/UI/Main/Contacts/ViewModel/FavoriteContactsListViewModel.swift @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import linphonesw + +class FavoriteContactsListViewModel: ObservableObject { + + init() {} +} diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 32bc8f301..ee2bb49c3 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -18,252 +18,421 @@ */ import SwiftUI +import linphonesw struct ContentView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel - @ObservedObject var contactViewModel: ContactViewModel - @ObservedObject var historyViewModel: HistoryViewModel - @ObservedObject private var coreContext = CoreContext.shared - - @State var index = 0 - @State private var orientation = UIDevice.current.orientation - @State var menuOpen: Bool = false - - var body: some View { - if !sharedMainViewModel.welcomeViewDisplayed { - WelcomeView(sharedMainViewModel: sharedMainViewModel) - } else if coreContext.mCore.defaultAccount == nil || sharedMainViewModel.displayProfileMode { - AssistantView(sharedMainViewModel: sharedMainViewModel) - } else { - GeometryReader { geometry in - ZStack { - VStack(spacing: 0) { - HStack(spacing: 0) { - if orientation == .landscapeLeft - || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { - VStack { - Group { - Spacer() - Button(action: { - self.index = 0 - }, label: { - VStack { - Image("address-book") - .renderingMode(.template) - .resizable() - .foregroundStyle(self.index == 0 ? Color.orangeMain500 : Color.grayMain2c600) - .frame(width: 25, height: 25) - if self.index == 0 { - Text("Contacts") - .default_text_style_700(styleSize: 10) - } else { - Text("Contacts") - .default_text_style(styleSize: 10) + var contactManager = ContactsManager.shared + var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject private var coreContext = CoreContext.shared + + @State var index = 0 + @State private var orientation = UIDevice.current.orientation + @State var sideMenuIsOpen: Bool = false + + @State private var searchIsActive = false + @State private var text = "" + @FocusState private var focusedField: Bool + @State var isMenuOpen: Bool = false + + var body: some View { + if !sharedMainViewModel.welcomeViewDisplayed { + WelcomeView(sharedMainViewModel: sharedMainViewModel) + } else if coreContext.mCore.defaultAccount == nil || sharedMainViewModel.displayProfileMode { + AssistantView(sharedMainViewModel: sharedMainViewModel) + } else { + GeometryReader { geometry in + ZStack { + VStack(spacing: 0) { + HStack(spacing: 0) { + if orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + VStack { + Group { + Spacer() + Button(action: { + self.index = 0 + }, label: { + VStack { + Image("address-book") + .renderingMode(.template) + .resizable() + .foregroundStyle(self.index == 0 ? Color.orangeMain500 : Color.grayMain2c600) + .frame(width: 25, height: 25) + if self.index == 0 { + Text("Contacts") + .default_text_style_700(styleSize: 10) + } else { + Text("Contacts") + .default_text_style(styleSize: 10) + } + } + }) + + Spacer() + + Button(action: { + self.index = 1 + contactViewModel.contactTitle = "" + }, label: { + VStack { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(self.index == 1 ? Color.orangeMain500 : Color.grayMain2c600) + .frame(width: 25, height: 25) + if self.index == 1 { + Text("Calls") + .default_text_style_700(styleSize: 10) + } else { + Text("Calls") + .default_text_style(styleSize: 10) + } + } + }) + + Spacer() + } + } + .frame(width: 75) + .padding(.leading, + orientation == .landscapeRight && geometry.safeAreaInsets.bottom > 0 + ? -geometry.safeAreaInsets.leading + : 0) + } + + VStack(spacing: 0) { + if searchIsActive == false { + HStack { + Image("profile-image-example") + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + .onTapGesture { + openMenu() + } + + Text(index == 0 ? "Contacts" : "Calls") + .default_text_style_white_800(styleSize: 20) + .padding(.leading, 10) + + Spacer() + + Button { + withAnimation { + searchIsActive.toggle() + } + } label: { + Image("search") + } + + Menu { + Button { + isMenuOpen = false + magicSearch.allContact = true + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + HStack { + Text("See all") + Spacer() + if magicSearch.allContact { + Image("green-check") + } } } - }) - - Spacer() - - Button(action: { - self.index = 1 - contactViewModel.contactTitle = "" - }, label: { - VStack { - Image("phone") - .renderingMode(.template) - .resizable() - .foregroundStyle(self.index == 1 ? Color.orangeMain500 : Color.grayMain2c600) - .frame(width: 25, height: 25) - if self.index == 1 { - Text("Calls") - .default_text_style_700(styleSize: 10) - } else { - Text("Calls") - .default_text_style(styleSize: 10) + + Button { + isMenuOpen = false + magicSearch.allContact = false + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + HStack { + Text("See Linphone contact") + Spacer() + if !magicSearch.allContact { + Image("green-check") + } } } - }) - - Spacer() - } - } - .frame(width: 75) - .padding(.leading, - orientation == .landscapeRight && geometry.safeAreaInsets.bottom > 0 - ? -geometry.safeAreaInsets.leading - : 0) - } - - VStack(spacing: 0) { - HStack { - Image("profile-image-example") - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) + } label: { + Image(index == 0 ? "filtres" : "more") + } + .padding(.leading) .onTapGesture { - openMenu() + isMenuOpen = true } - - Text(index == 0 ? "Contacts" : "Calls") - .default_text_style_white_800(styleSize: 20) - .padding(.leading, 10) - - Spacer() - - Button { - - } label: { - Image("search") - } - - Button { - - } label: { - Image(index == 0 ? "filtres" : "more") - } - .padding(.leading) - } - .frame(maxWidth: .infinity) - .frame(height: 50) - .padding(.horizontal) - .background(Color.orangeMain500) - - if self.index == 0 { - ContactsView(contactViewModel: contactViewModel, historyViewModel: historyViewModel) - } else if self.index == 1 { - HistoryView() - } - } - .frame(maxWidth: - (orientation == .landscapeLeft - || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) - ? geometry.size.width/100*40 - : .infinity - ) - .background( - Color.white - .shadow(color: Color.gray200, radius: 4, x: 0, y: 0) - .mask(Rectangle().padding(.horizontal, -8)) - ) - - if orientation == .landscapeLeft - || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { - Spacer() - } - } - - if !(orientation == .landscapeLeft - || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { - HStack { - Group { - Spacer() - Button(action: { - self.index = 0 - }, label: { - VStack { - Image("address-book") - .renderingMode(.template) - .resizable() - .foregroundStyle(self.index == 0 ? Color.orangeMain500 : Color.grayMain2c600) - .frame(width: 25, height: 25) - if self.index == 0 { - Text("Contacts") - .default_text_style_700(styleSize: 10) - } else { - Text("Contacts") - .default_text_style(styleSize: 10) - } - } - }) - .padding(.top) - - Spacer() - - Button(action: { - self.index = 1 - contactViewModel.contactTitle = "" - }, label: { - VStack { - Image("phone") - .renderingMode(.template) - .resizable() - .foregroundStyle(self.index == 1 ? Color.orangeMain500 : Color.grayMain2c600) - .frame(width: 25, height: 25) - if self.index == 1 { - Text("Calls") - .default_text_style_700(styleSize: 10) - } else { - Text("Calls") - .default_text_style(styleSize: 10) - } - } - }) - .padding(.top) - Spacer() - } - } - .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 15) - .background( - Color.white - .shadow(color: Color.gray200, radius: 4, x: 0, y: 0) - .mask(Rectangle().padding(.top, -8)) - ) - } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 5) + .background(Color.orangeMain500) + } else { + HStack { + Button { + withAnimation { + self.focusedField = false + searchIsActive.toggle() + } + + text = "" + magicSearch.currentFilter = "" + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + } + + if #available(iOS 16.0, *) { + TextEditor(text: Binding( + get: { + return text + }, + set: { value in + var newValue = value + if value.contains("\n") { + newValue = value.replacingOccurrences(of: "\n", with: "") + } + text = newValue + } + )) + .default_text_style_white_700(styleSize: 15) + .padding(.all, 6) + .accentColor(.white) + .scrollContentBackground(.hidden) + .focused($focusedField) + .onAppear { + self.focusedField = true + } + .onChange(of: text) { newValue in + magicSearch.currentFilter = newValue + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + } else { + TextEditor(text: Binding( + get: { + return text + }, + set: { value in + var newValue = value + if value.contains("\n") { + newValue = value.replacingOccurrences(of: "\n", with: "") + } + text = newValue + } + )) + .default_text_style_white_700(styleSize: 15) + .padding(.all, 6) + .accentColor(.white) + .focused($focusedField) + .onAppear { + self.focusedField = true + } + .onChange(of: text) { newValue in + magicSearch.currentFilter = newValue + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + } + + Button { + text = "" + } label: { + Image("x") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + } + .padding(.leading) + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 5) + .background(Color.orangeMain500) + } + + if self.index == 0 { + ContactsView(contactViewModel: contactViewModel, historyViewModel: historyViewModel) + } else if self.index == 1 { + HistoryView() + } + } + .frame(maxWidth: + (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) + ? geometry.size.width/100*40 + : .infinity + ) + .background( + Color.white + .shadow(color: Color.gray200, radius: 4, x: 0, y: 0) + .mask(Rectangle().padding(.horizontal, -8)) + ) + + if orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + Spacer() + } + } + + if !(orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) && !searchIsActive { + HStack { + Group { + Spacer() + Button(action: { + self.index = 0 + }, label: { + VStack { + Image("address-book") + .renderingMode(.template) + .resizable() + .foregroundStyle(self.index == 0 ? Color.orangeMain500 : Color.grayMain2c600) + .frame(width: 25, height: 25) + if self.index == 0 { + Text("Contacts") + .default_text_style_700(styleSize: 10) + } else { + Text("Contacts") + .default_text_style(styleSize: 10) + } + } + }) + .padding(.top) + + Spacer() + + Button(action: { + self.index = 1 + contactViewModel.contactTitle = "" + }, label: { + VStack { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(self.index == 1 ? Color.orangeMain500 : Color.grayMain2c600) + .frame(width: 25, height: 25) + if self.index == 1 { + Text("Calls") + .default_text_style_700(styleSize: 10) + } else { + Text("Calls") + .default_text_style(styleSize: 10) + } + } + }) + .padding(.top) + Spacer() + } + } + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 15) + .background( + Color.white + .shadow(color: Color.gray200, radius: 4, x: 0, y: 0) + .mask(Rectangle().padding(.top, -8)) + ) + } + } + + if !contactViewModel.contactTitle.isEmpty || !historyViewModel.historyTitle.isEmpty { + HStack(spacing: 0) { + Spacer() + .frame(maxWidth: + (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) + ? (geometry.size.width/100*40) + 75 + : 0 + ) + if self.index == 0 { + ContactFragment(contactViewModel: contactViewModel) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) + } else if self.index == 1 { + HistoryContactFragment() + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) + } + } + .onAppear { + if !(orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) + && searchIsActive { + self.focusedField = false + } + } + .onDisappear { + if !(orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) + && searchIsActive { + self.focusedField = true + } + } + .padding(.leading, + orientation == .landscapeRight && geometry.safeAreaInsets.bottom > 0 + ? -geometry.safeAreaInsets.leading + : 0) + .transition(.move(edge: .trailing)) + .zIndex(1) + } + + SideMenu( + width: geometry.size.width / 5 * 4, + isOpen: self.sideMenuIsOpen, + menuClose: self.openMenu, + safeAreaInsets: geometry.safeAreaInsets + ) + .ignoresSafeArea(.all) + .zIndex(2) + } + } + .overlay { + if isMenuOpen { + Color.white.opacity(0.001) + .ignoresSafeArea() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onTapGesture { + isMenuOpen = false } - - if !contactViewModel.contactTitle.isEmpty || !historyViewModel.historyTitle.isEmpty { - HStack(spacing: 0) { - Spacer() - .frame(maxWidth: - (orientation == .landscapeLeft - || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) - ? (geometry.size.width/100*40) + 75 - : 0 - ) - if self.index == 0 { - ContactFragment(contactViewModel: contactViewModel) - .frame(maxWidth: .infinity) - .background(Color.gray100) - } else if self.index == 1 { - HistoryContactFragment() - .frame(maxWidth: .infinity) - .background(Color.gray100) - } - } - .padding(.leading, - orientation == .landscapeRight && geometry.safeAreaInsets.bottom > 0 - ? -geometry.safeAreaInsets.leading - : 0) - .transition(.move(edge: .trailing)) - } - - SideMenu( - width: geometry.size.width / 5 * 4, - isOpen: self.menuOpen, - menuClose: self.openMenu, - safeAreaInsets: geometry.safeAreaInsets - ) - .ignoresSafeArea(.all) } } - .onRotate { newOrientation in - orientation = newOrientation - } - } - } - - func openMenu() { - withAnimation { - self.menuOpen.toggle() - } - } + .onRotate { newOrientation in + if (!contactViewModel.contactTitle.isEmpty || !historyViewModel.historyTitle.isEmpty) && searchIsActive { + self.focusedField = false + } else if searchIsActive { + self.focusedField = true + } + orientation = newOrientation + } + } + } + + func openMenu() { + withAnimation { + self.sideMenuIsOpen.toggle() + } + } } #Preview { diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift new file mode 100644 index 000000000..2ee38c8cc --- /dev/null +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import linphonesw + +final class MagicSearchSingleton: ObservableObject { + + static let shared = MagicSearchSingleton() + private var coreContext = CoreContext.shared + + private var magicSearch: MagicSearch! + var magicSearchDelegate: MagicSearchDelegate? + + @objc var currentFilter: String = "" + var previousFilter: String? + + var needUpdateLastSearchContacts = false + + @Published var lastSearch: [SearchResult] = [] + + private var limitSearchToLinphoneAccounts = true + + @Published var allContact = false + private var domainDefaultAccount = "" + + private init() { + domainDefaultAccount = coreContext.mCore.defaultAccount!.params!.domain! + + magicSearch = try? coreContext.mCore.createMagicSearch() + magicSearch.limitedSearch = false + + magicSearchDelegate = MagicSearchDelegateStub(onSearchResultsReceived: { (magicSearch: MagicSearch) in + self.needUpdateLastSearchContacts = true + self.lastSearch = magicSearch.lastSearch + }) + + magicSearch.addDelegate(delegate: magicSearchDelegate!) + } + + func searchForContacts(sourceFlags: Int) { + if let oldFilter = previousFilter { + if oldFilter.count > currentFilter.count || oldFilter != currentFilter { + magicSearch.resetSearchCache() + } + } + previousFilter = currentFilter + + magicSearch.getContactsListAsync( + filter: currentFilter, + domain: allContact ? "" : domainDefaultAccount, + sourceFlags: sourceFlags, + aggregation: MagicSearch.Aggregation.Friend) + } +} diff --git a/Linphone/Utils/PermissionManager.swift b/Linphone/Utils/PermissionManager.swift index a39833010..f1d741f7e 100644 --- a/Linphone/Utils/PermissionManager.swift +++ b/Linphone/Utils/PermissionManager.swift @@ -19,6 +19,7 @@ import Foundation import Photos +import Contacts class PermissionManager: ObservableObject { @@ -26,6 +27,7 @@ class PermissionManager: ObservableObject { @Published var photoLibraryPermissionGranted = false @Published var cameraPermissionGranted = false + @Published var contactsPermissionGranted = false private init() {} @@ -44,4 +46,13 @@ class PermissionManager: ObservableObject { } }) } + + func contactsRequestPermission() { + let store = CNContactStore() + store.requestAccess(for: .contacts) { success, _ in + DispatchQueue.main.async { + self.contactsPermissionGranted = success + } + } + } } diff --git a/Linphone/Utils/TextExtension.swift b/Linphone/Utils/TextExtension.swift index e9d58ae0b..8465dfbff 100644 --- a/Linphone/Utils/TextExtension.swift +++ b/Linphone/Utils/TextExtension.swift @@ -136,4 +136,9 @@ extension View { self.font(Font.custom("NotoSans-Regular", size: styleSize)) .foregroundStyle(Color.grayMain2c600) } + + func contact_text_style_500(styleSize: CGFloat) -> some View { + self.font(Font.custom("NotoSans-Medium", size: styleSize)) + .foregroundStyle(Color.grayMain2c400) + } }