diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 64f20df16..3636ae410 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -27,8 +27,17 @@ D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343312ACEFF58009AA24E /* QRScannerController.swift */; }; D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343332ACEFFC3009AA24E /* QRScanner.swift */; }; D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.swift */; }; + D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E4382B16440C0083C415 /* ContactAvatarModel.swift */; }; + D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */; }; + D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */; }; D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; }; D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; }; + D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */ = {isa = PBXBuildFile; fileRef = D732A90A2B0376F500DB42BA /* linphonerc-default */; }; + D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */ = {isa = PBXBuildFile; fileRef = D732A90B2B0376F500DB42BA /* linphonerc-factory */; }; + D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */; }; + D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */; }; + D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */; }; + D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.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 */; }; @@ -37,14 +46,20 @@ D74C9CFF2ACAEC5E0021626A /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CFE2ACAEC5E0021626A /* PopupView.swift */; }; D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9D002ACB098C0021626A /* PermissionManager.swift */; }; D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */; }; + D76005F62B0798B00054B79A /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76005F52B0798B00054B79A /* IntExtension.swift */; }; D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7702EF12AC7205000557C00 /* WelcomeView.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 */; }; + D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */; }; + D783C77D2B1089B200622CC2 /* assistant_third_party_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */; }; + D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79622332B1DFE600037EACD /* DialerBottomSheet.swift */; }; + D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.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 */; }; + D7ADF6002AFE356400212231 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADF5FF2AFE356400212231 /* Avatar.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 */; }; @@ -93,8 +108,17 @@ D72343312ACEFF58009AA24E /* QRScannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; D72343332ACEFFC3009AA24E /* QRScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanner.swift; sourceTree = ""; }; D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; + D726E4382B16440C0083C415 /* ContactAvatarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAvatarModel.swift; sourceTree = ""; }; + D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallFragment.swift; sourceTree = ""; }; + D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.swift; sourceTree = ""; }; D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = ""; }; D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = ""; }; + D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = ""; }; + D732A90B2B0376F500DB42BA /* linphonerc-factory */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-factory"; sourceTree = ""; }; + D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFragment.swift; sourceTree = ""; }; + D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = ""; }; + D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = ""; }; + D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListBottomSheet.swift; sourceTree = ""; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = ""; }; @@ -103,15 +127,21 @@ D74C9CFE2ACAEC5E0021626A /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = ""; }; D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = ""; }; + D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.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 = ""; }; + D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = ""; }; + D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = ""; }; + D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = ""; }; + D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.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 = ""; }; + D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = ""; }; D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = ""; }; D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = ""; }; @@ -160,11 +190,13 @@ children = ( D717071D2AC5922E0037746F /* ColorExtension.swift */, D717071F2AC5989C0037746F /* TextExtension.swift */, + D76005F52B0798B00054B79A /* IntExtension.swift */, D74C9D002ACB098C0021626A /* PermissionManager.swift */, D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */, D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, D7C48DF32AFA66F900D938CB /* EditContactController.swift */, D732A9082AFD235500DB42BA /* ShareSheetController.swift */, + D7ADF5FF2AFE356400212231 /* Avatar.swift */, ); path = Utils; sourceTree = ""; @@ -201,6 +233,7 @@ D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */, D719ABBD2ABC67BF00B41C10 /* Preview Content */, D7D24D0C2AC1B4C700C6F35B /* Fonts */, + D7ADF6012AFE5C7C00212231 /* Ressources */, ); path = Linphone; sourceTree = ""; @@ -267,14 +300,29 @@ isa = PBXGroup; children = ( D72250622ADE9615008FB426 /* HistoryViewModel.swift */, + D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */, + D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */, ); path = ViewModel; sourceTree = ""; }; + D726E4372B1643FF0083C415 /* Model */ = { + isa = PBXGroup; + children = ( + D726E4382B16440C0083C415 /* ContactAvatarModel.swift */, + ); + path = Model; + sourceTree = ""; + }; D72992372ADD7F1C003AF125 /* Fragments */ = { isa = PBXGroup; children = ( D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */, + D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */, + D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */, + D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */, + D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */, + D79622332B1DFE600037EACD /* DialerBottomSheet.swift */, ); path = Fragments; sourceTree = ""; @@ -351,6 +399,7 @@ isa = PBXGroup; children = ( D78290B62ADD38F9004AA85C /* Fragments */, + D726E4372B1643FF0083C415 /* Model */, D78290B92ADD409D004AA85C /* ViewModel */, D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */, ); @@ -371,10 +420,22 @@ isa = PBXGroup; children = ( D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */, + D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */, ); path = Viewmodel; sourceTree = ""; }; + D7ADF6012AFE5C7C00212231 /* Ressources */ = { + isa = PBXGroup; + children = ( + D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */, + D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */, + D732A90A2B0376F500DB42BA /* linphonerc-default */, + D732A90B2B0376F500DB42BA /* linphonerc-factory */, + ); + path = Ressources; + sourceTree = ""; + }; D7D24D0C2AC1B4C700C6F35B /* Fonts */ = { isa = PBXGroup; children = ( @@ -464,11 +525,15 @@ D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */, D7D24D182AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf in Resources */, D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */, + D783C77D2B1089B200622CC2 /* assistant_third_party_default_values in Resources */, D7D24D162AC1B4E800C6F35B /* NotoSans-SemiBold.ttf in Resources */, D7D24D172AC1B4E800C6F35B /* NotoSans-Bold.ttf in Resources */, D719ABBF2ABC67BF00B41C10 /* Preview Assets.xcassets in Resources */, D719ABBB2ABC67BF00B41C10 /* Assets.xcassets in Resources */, D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */, + D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */, + D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */, + D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */, D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -503,6 +568,7 @@ buildActionMask = 2147483647; files = ( D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */, + D7ADF6002AFE356400212231 /* Avatar.swift in Sources */, D71707202AC5989C0037746F /* TextExtension.swift in Sources */, D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */, D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */, @@ -510,12 +576,16 @@ D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */, D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */, D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */, + D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */, D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */, D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */, D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */, + D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */, D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */, D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, + D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, + D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */, @@ -523,10 +593,14 @@ D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, + D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */, D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */, D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, + D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, + D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */, + D76005F62B0798B00054B79A /* IntExtension.swift in Sources */, D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */, @@ -541,7 +615,9 @@ D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */, D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */, D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, + D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */, D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */, + D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */, diff --git a/Linphone/.DS_Store b/Linphone/.DS_Store index ee98363d4..06ce575e5 100644 Binary files a/Linphone/.DS_Store and b/Linphone/.DS_Store differ diff --git a/Linphone/Assets.xcassets/dialer.imageset/Contents.json b/Linphone/Assets.xcassets/dialer.imageset/Contents.json new file mode 100644 index 000000000..117f088cd --- /dev/null +++ b/Linphone/Assets.xcassets/dialer.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dialer.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/dialer.imageset/dialer.svg b/Linphone/Assets.xcassets/dialer.imageset/dialer.svg new file mode 100644 index 000000000..71705dfdf --- /dev/null +++ b/Linphone/Assets.xcassets/dialer.imageset/dialer.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json b/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json new file mode 100644 index 000000000..227036d8d --- /dev/null +++ b/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "presence-busy.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg b/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg new file mode 100644 index 000000000..0f24966ca --- /dev/null +++ b/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Assets.xcassets/presence-online.imageset/Contents.json b/Linphone/Assets.xcassets/presence-online.imageset/Contents.json new file mode 100644 index 000000000..606200f2a --- /dev/null +++ b/Linphone/Assets.xcassets/presence-online.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "presence-online.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg b/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg new file mode 100644 index 000000000..ae3b6ed5e --- /dev/null +++ b/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json b/Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json new file mode 100644 index 000000000..02ba54e4e --- /dev/null +++ b/Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "trash-simple-red.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/trash-simple-red.imageset/trash-simple-red.svg b/Linphone/Assets.xcassets/trash-simple-red.imageset/trash-simple-red.svg new file mode 100644 index 000000000..ea7d36f6a --- /dev/null +++ b/Linphone/Assets.xcassets/trash-simple-red.imageset/trash-simple-red.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index fa9843527..0b00df121 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -22,12 +22,11 @@ import Contacts import SwiftUI import ContactsUI -final class ContactsManager { +final class ContactsManager: ObservableObject { static let shared = ContactsManager() private var coreContext = CoreContext.shared - private var magicSearch = MagicSearchSingleton.shared private let nativeAddressBookFriendList = "Native address-book" let linphoneAddressBookFriendList = "Linphone address-book" @@ -35,6 +34,10 @@ final class ContactsManager { var friendList: FriendList? var linphoneFriendList: FriendList? + @Published var lastSearch: [SearchResult] = [] + @Published var lastSearchSuggestions: [SearchResult] = [] + @Published var avatarListModel: [ContactAvatarModel] = [] + private init() { fetchContacts() } @@ -119,7 +122,8 @@ final class ContactsManager { && 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" : ""), + name: contact.givenName + contact.familyName, + prefix: ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: nil) } }) @@ -132,7 +136,8 @@ final class ContactsManager { print("\(#function) - access denied") } } - self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + + MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } } @@ -164,16 +169,16 @@ final class ContactsManager { return IBImgViewUserProfile } - func saveImage(image: UIImage, name: String, contact: Contact, linphoneFriend: Bool, existingFriend: Friend?) { + func saveImage(image: UIImage, name: String, prefix: 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 + awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in self.saveFriend(result: result, contact: contact, existingFriend: existingFriend) { resultFriend in if resultFriend != nil { if linphoneFriend && existingFriend == nil { - _ = self.linphoneFriendList?.addLocalFriend(linphoneFriend: resultFriend!) + _ = self.linphoneFriendList?.addFriend(linphoneFriend: resultFriend!) self.linphoneFriendList?.updateSubscriptions() } else if existingFriend == nil { _ = self.friendList?.addLocalFriend(linphoneFriend: resultFriend!) @@ -188,7 +193,7 @@ final class ContactsManager { 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) @@ -236,7 +241,11 @@ final class ContactsManager { friend.organization = contact.organizationName friend.jobTitle = contact.jobTitle + try friend.setSubscribesenabled(newValue: false) + try friend.setIncsubscribepolicy(newValue: .SPDeny) + friend.done() + completion(friend) } catch let error { print("Failed to enumerate contact", error) @@ -253,15 +262,17 @@ final class ContactsManager { return imagePath } - func awaitDataWrite(data: Data, name: String, completion: @escaping ((), String) -> Void) { + func awaitDataWrite(data: Data, name: String, prefix: 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 urlName = URL(string: name + prefix) + let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : "ImageError" + let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png")) + completion(decodedData, imagePath + ".png") } catch { print("Error: ", error) @@ -271,9 +282,24 @@ final class ContactsManager { } } - func getFriend(contact: Contact) -> Friend? { + func getFriendWithContact(contact: Contact) -> Friend? { if friendList != nil { let friend = friendList!.friends.first(where: {$0.nativeUri == contact.identifier}) + if friend == nil && friendList != nil { + return linphoneFriendList!.friends.first(where: {$0.nativeUri == contact.identifier}) + } + return friend + } else { + return nil + } + } + + func getFriendWithAddress(address: Address) -> Friend? { + if friendList != nil { + var friend = friendList!.friends.first(where: {$0.addresses.contains(where: {$0.asStringUriOnly() == address.asStringUriOnly()})}) + if friend == nil { + friend = linphoneFriendList!.friends.first(where: {$0.addresses.contains(where: {$0.asStringUriOnly() == address.asStringUriOnly()})}) + } return friend } else { return nil diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 350fb6a0d..7d9bb5e6e 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -24,17 +24,24 @@ import Combine final class CoreContext: ObservableObject { static let shared = CoreContext() + private var sharedMainViewModel = SharedMainViewModel.shared var coreVersion: String = Core.getVersion @Published var loggedIn: Bool = false @Published var loggingInProgress: Bool = false - @Published var toastMessage: String = "" @Published var defaultAccount: Account? + @Published var coreIsStarted: Bool = false private var mCore: Core! private var mIteratePublisher: AnyCancellable? - private init() {} + private init() { + do { + try initialiseCore() + } catch { + + } + } func doOnCoreQueue(synchronous: Bool = false, lambda: @escaping (Core) -> Void) { if synchronous { @@ -53,17 +60,45 @@ final class CoreContext: ObservableObject { coreQueue.async { let configDir = Factory.Instance.getConfigDir(context: nil) - try? self.mCore = Factory.Instance.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) + + Factory.Instance.logCollectionPath = configDir + Factory.Instance.enableLogCollection(state: LogCollectionState.Enabled) + + let url = NSURL(fileURLWithPath: configDir) + if let pathComponent = url.appendingPathComponent("linphonerc") { + let filePath = pathComponent.path + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: filePath) { + let path = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) + if path != nil { + try? FileManager.default.copyItem(at: NSURL(fileURLWithPath: path!) as URL, to: pathComponent) + } + } + } + + let config = try? Factory.Instance.createConfigWithFactory( + path: "\(configDir)/linphonerc", + factoryPath: Bundle.main.path(forResource: "linphonerc-factory", ofType: nil) + ) + if config != nil { + self.mCore = try? Factory.Instance.createCoreWithConfig(config: config!, systemContext: nil) + } + self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" + self.mCore.friendListSubscriptionEnabled = true + self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { self.defaultAccount = self.mCore.defaultAccount + self.coreIsStarted = true } else if cbVal.state == GlobalState.Off { self.defaultAccount = nil + self.coreIsStarted = true } } + try? self.mCore.start() // Create a Core listener to listen for the callback we need @@ -71,9 +106,11 @@ final class CoreContext: ObservableObject { self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in NSLog("New configuration state is \(cbVal.status) = \(cbVal.message)\n") if cbVal.status == Config.ConfiguringState.Successful { - self.toastMessage = "Successful" + ToastViewModel.shared.toastMessage = "Successful" + ToastViewModel.shared.displayToast.toggle() } else { - self.toastMessage = "Failed" + ToastViewModel.shared.toastMessage = "Failed" + ToastViewModel.shared.displayToast.toggle() } } @@ -87,10 +124,14 @@ final class CoreContext: ObservableObject { if cbVal.state == .Ok { self.loggingInProgress = false self.loggedIn = true + if self.mCore.consolidatedPresence != ConsolidatedPresence.Online { + self.onForeground() + } } else if cbVal.state == .Progress { self.loggingInProgress = true } else { - self.toastMessage = "Registration failed" + ToastViewModel.shared.toastMessage = "Registration failed" + ToastViewModel.shared.displayToast.toggle() self.loggingInProgress = false self.loggedIn = false } @@ -117,6 +158,30 @@ final class CoreContext: ObservableObject { } } + + func onForeground() { + coreQueue.async { + // We can't rely on defaultAccount?.params?.isPublishEnabled + // as it will be modified by the SDK when changing the presence status + if self.mCore.config!.getBool(section: "app", key: "publish_presence", defaultValue: true) { + NSLog("App is in foreground, PUBLISHING presence as Online") + self.mCore.consolidatedPresence = ConsolidatedPresence.Online + } + } + } + + func onBackground() { + coreQueue.async { + // We can't rely on defaultAccount?.params?.isPublishEnabled + // as it will be modified by the SDK when changing the presence status + if self.mCore.config!.getBool(section: "app", key: "publish_presence", defaultValue: true) { + NSLog("App is in background, un-PUBLISHING presence info") + // We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe, + // Flexisip will handle the Busy status depending on other devices + self.mCore.consolidatedPresence = ConsolidatedPresence.Offline + } + } + } } // swiftlint:enable large_tuple diff --git a/Linphone/Linphone.entitlements b/Linphone/Linphone.entitlements index f2ef3ae02..7fe270b49 100644 --- a/Linphone/Linphone.entitlements +++ b/Linphone/Linphone.entitlements @@ -2,9 +2,15 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.belledonne-communications.linphone + group.org.linphone.phone.linphoneExtension + group.org.linphone.phone.msgNotification + + com.apple.security.files.user-selected.read-only + diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 85f14b81c..61e22bdf3 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -23,24 +23,46 @@ import SwiftUI struct LinphoneApp: App { @ObservedObject private var coreContext = CoreContext.shared - @ObservedObject private var sharedMainViewModel = SharedMainViewModel() + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared - @State private var isActive = false + @State private var contactViewModel: ContactViewModel? + @State private var editContactViewModel: EditContactViewModel? + @State private var historyViewModel: HistoryViewModel? + @State private var historyListViewModel: HistoryListViewModel? + @State private var startCallViewModel: StartCallViewModel? var body: some Scene { WindowGroup { - if isActive { + if coreContext.coreIsStarted { if !sharedMainViewModel.welcomeViewDisplayed { - WelcomeView(sharedMainViewModel: sharedMainViewModel) + WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { - AssistantView(sharedMainViewModel: sharedMainViewModel) - .toast(isShowing: $coreContext.toastMessage) - } else if coreContext.defaultAccount != nil { - ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel()) - .toast(isShowing: $coreContext.toastMessage) + AssistantView() + } else if coreContext.defaultAccount != nil + && contactViewModel != nil + && editContactViewModel != nil + && historyViewModel != nil + && historyListViewModel != nil + && startCallViewModel != nil { + ContentView( + contactViewModel: contactViewModel!, + editContactViewModel: editContactViewModel!, + historyViewModel: historyViewModel!, + historyListViewModel: historyListViewModel!, + startCallViewModel: startCallViewModel! + ) + } else { + SplashScreen() } } else { - SplashScreen(isActive: $isActive) + SplashScreen() + .onDisappear { + contactViewModel = ContactViewModel() + editContactViewModel = EditContactViewModel() + historyViewModel = HistoryViewModel() + historyListViewModel = HistoryListViewModel() + startCallViewModel = StartCallViewModel() + } } } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 0062a8ce1..569f38f71 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -27,6 +27,9 @@ }, "[notre politique de confidentialité](https://linphone.org/privacy-policy)" : { + }, + "*" : { + }, "**Camera** : Pour capturer votre vidéo lors des appels vidéo et conférence." : { @@ -45,6 +48,9 @@ }, "**Notifications** : Pour vous informé quand vous recevez un message ou un appel." : { + }, + "#" : { + }, "%lld Book (Example)" : { "extractionState" : "manual", @@ -98,15 +104,57 @@ } } } + }, + "+" : { + + }, + "0" : { + + }, + "1" : { + + }, + "2" : { + + }, + "3" : { + + }, + "4" : { + + }, + "5" : { + + }, + "6" : { + + }, + "7" : { + + }, + "8" : { + + }, + "9" : { + }, "Accept all" : { }, "Add a picture" : { + }, + "Add the contact" : { + + }, + "Add to contacts" : { + }, "Add to favourites" : { + }, + "All calls will be removed from the history." : { + }, "All contacts" : { @@ -139,6 +187,9 @@ }, "Block the number" : { + }, + "Call history" : { + }, "Calls" : { @@ -151,6 +202,9 @@ }, "Chiffrement de bout en bout de tous vos échanges, grâce au mode default vos communications sont à l’abri des regards." : { + }, + "Clear logs" : { + }, "Close" : { @@ -172,6 +226,9 @@ }, "Copy number" : { + }, + "Copy SIP address" : { + }, "D'accord" : { @@ -187,6 +244,12 @@ }, "Delete %@?" : { + }, + "Delete all history" : { + + }, + "Delete history" : { + }, "Delete this contact" : { @@ -199,6 +262,9 @@ }, "Display Name" : { + }, + "Do you really want to delete all calls history?" : { + }, "Domain" : { @@ -220,9 +286,6 @@ }, "En continuant, vous acceptez ces conditions, " : { - }, - "En ligne" : { - }, "Error" : { @@ -239,7 +302,7 @@ "First name*" : { }, - "History Contact fragment" : { + "History has been deleted" : { }, "I prefere create an account" : { @@ -247,6 +310,9 @@ }, "I understand" : { + }, + "Incoming Call" : { + }, "Information" : { @@ -286,6 +352,9 @@ }, "My Profile" : { + }, + "New call" : { + }, "New contact" : { @@ -293,7 +362,7 @@ "Next" : { }, - "No calls for the moment..." : { + "No call for the moment..." : { }, "No contacts for the moment..." : { @@ -313,6 +382,9 @@ }, "Other actions" : { + }, + "Outgoing Call" : { + }, "password" : { "extractionState" : "manual", @@ -345,9 +417,6 @@ }, "Plus tard" : { - }, - "Posts" : { - }, "Pour vous permettre de vous profitez pleinement de Linphone nous avons besoin des autorisations suivantes :" : { @@ -366,15 +435,24 @@ }, "Scan QR code" : { + }, + "Search contact or history call" : { + }, "Sécurisé" : { }, "See all" : { + }, + "See contact" : { + }, "See Linphone contact" : { + }, + "Send logs" : { + }, "Share" : { @@ -384,6 +462,9 @@ }, "SIP address :" : { + }, + "SIP address copied into clipboard" : { + }, "sip.linphone.org" : { @@ -393,6 +474,9 @@ }, "Start" : { + }, + "Suggestions" : { + }, "TCP" : { @@ -443,6 +527,9 @@ } } } + }, + "Username error" : { + }, "Video Call" : { diff --git a/Linphone/Ressources/assistant_linphone_default_values b/Linphone/Ressources/assistant_linphone_default_values new file mode 100644 index 000000000..f3723d7a9 --- /dev/null +++ b/Linphone/Ressources/assistant_linphone_default_values @@ -0,0 +1,36 @@ + + +
+ 1 + 0 + 1 + 120 + sip:voip-metrics@sip.linphone.org;transport=tls + 1 + 180 + 31536000 + sip:?@sip.linphone.org + <sip:sip.linphone.org;transport=tls> + <sip:sip.linphone.org;transport=tls> + 1 + nat_policy_default_values + sip.linphone.org + sip:conference-factory@sip.linphone.org + sip:videoconference-factory@sip.linphone.org + 1 + 1 + 1 + https://lime.linphone.org/lime-server/lime-server.php +
+
+ stun.linphone.org + stun,ice +
+
+ zrtp + 1 +
+
+ 1 +
+
diff --git a/Linphone/Ressources/assistant_third_party_default_values b/Linphone/Ressources/assistant_third_party_default_values new file mode 100644 index 000000000..78927cf4e --- /dev/null +++ b/Linphone/Ressources/assistant_third_party_default_values @@ -0,0 +1,25 @@ + + +
+ 0 + 0 + 0 + -1 + + 0 + 0 + 3600 + + + + 1 + + + + + 0 + 0 + 0 + +
+
diff --git a/Linphone/Ressources/linphonerc-default b/Linphone/Ressources/linphonerc-default new file mode 100644 index 000000000..ea5356429 --- /dev/null +++ b/Linphone/Ressources/linphonerc-default @@ -0,0 +1,39 @@ + +## Start of default rc + +[sip] +contact="Linphone iPhone" +use_info=0 +use_ipv6=1 +keepalive_period=30000 +sip_port=-1 +sip_tcp_port=-1 +sip_tls_port=-1 +media_encryption=none +update_presence_model_timestamp_before_publish_expires_refresh=1 + +[net] +#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit" +download_bw=0 +upload_bw=0 + +[video] +size=vga + +[app] +tunnel=disabled +auto_start=1 +record_aware=1 + +[tunnel] +host= +port=443 + +[misc] +log_collection_upload_server_url=https://www.linphone.org:444/lft.php +file_transfer_server_url=https://www.linphone.org:444/lft.php +version_check_url_root=https://www.linphone.org/releases +max_calls=10 +conference_layout=1 + +## End of default rc diff --git a/Linphone/Ressources/linphonerc-factory b/Linphone/Ressources/linphonerc-factory new file mode 100644 index 000000000..85b543074 --- /dev/null +++ b/Linphone/Ressources/linphonerc-factory @@ -0,0 +1,65 @@ + +## Start of factory rc + +# This file shall not contain path referencing package name, in order to be portable when app is renamed. +# Paths to resources must be set from LinphoneManager, after creating LinphoneCore. + +[net] +mtu=1300 +force_ice_disablement=0 + +[rtp] +accept_any_encryption=1 + +[sip] +guess_hostname=1 +register_only_when_network_is_up=1 +auto_net_state_mon=1 +auto_answer_replacing_calls=1 +ping_with_options=0 +use_cpim=1 +zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512 +chat_messages_aggregation_delay=1000 +chat_messages_aggregation=1 +update_presence_model_timestamp_before_publish_expires_refresh=1 +rls_uri=sips:rls@sip.linphone.org + +[sound] +#remove this property for any application that is not Linphone public version itself +ec_calibrator_cool_tones=1 + +[video] +displaytype=MSAndroidTextureDisplay +auto_resize_preview_to_keep_ratio=1 +max_conference_size=vga + +[misc] +enable_basic_to_client_group_chat_room_migration=0 +enable_simple_group_chat_message_state=0 +aggregate_imdn=1 +notify_each_friend_individually_when_presence_received=0 +store_friends=0 + +[app] +activation_code_length=4 +prefer_basic_chat_room=1 +record_aware=1 + +[account_creator] +backend=1 +# 1 means FlexiAPI, 0 is XMLRPC +url=https://subscribe.linphone.org/api/ +# replace above URL by https://staging-subscribe.linphone.org/api/ for testing + +[lime] +lime_update_threshold=86400 + +[alerts] +alerts_enabled=1 + +[assistant] +algorithm=SHA-256 +password_min_length=6 +username_regex=^[a-z0-9+_.\-]*$ + +## End of factory rc diff --git a/Linphone/SplashScreen.swift b/Linphone/SplashScreen.swift index 350fc93ad..e15c4aaac 100644 --- a/Linphone/SplashScreen.swift +++ b/Linphone/SplashScreen.swift @@ -21,9 +21,6 @@ import SwiftUI struct SplashScreen: View { - @ObservedObject private var coreContext = CoreContext.shared - @Binding var isActive: Bool - var body: some View { GeometryReader { _ in VStack { @@ -38,17 +35,9 @@ struct SplashScreen: View { } .ignoresSafeArea(.all) - .onAppear { - Task { - try coreContext.initialiseCore() - withAnimation { - self.isActive = true - } - } - } } } #Preview { - SplashScreen(isActive: .constant(true)) + SplashScreen() } diff --git a/Linphone/UI/Assistant/AssistantView.swift b/Linphone/UI/Assistant/AssistantView.swift index 1819486d8..6c4418bcf 100644 --- a/Linphone/UI/Assistant/AssistantView.swift +++ b/Linphone/UI/Assistant/AssistantView.swift @@ -21,18 +21,18 @@ import SwiftUI struct AssistantView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject private var coreContext = CoreContext.shared var body: some View { if sharedMainViewModel.displayProfileMode && coreContext.loggedIn { - ProfileModeFragment(sharedMainViewModel: sharedMainViewModel) + ProfileModeFragment() } else { - LoginFragment(accountLoginViewModel: AccountLoginViewModel(), sharedMainViewModel: sharedMainViewModel) + LoginFragment(accountLoginViewModel: AccountLoginViewModel()) } } } #Preview { - LoginFragment(accountLoginViewModel: AccountLoginViewModel(), sharedMainViewModel: SharedMainViewModel()) + LoginFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/LoginFragment.swift b/Linphone/UI/Assistant/Fragments/LoginFragment.swift index 61677f584..8ae3e46e1 100644 --- a/Linphone/UI/Assistant/Fragments/LoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/LoginFragment.swift @@ -22,8 +22,8 @@ import SwiftUI struct LoginFragment: View { @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel - @ObservedObject var sharedMainViewModel: SharedMainViewModel @State private var isSecured: Bool = true @@ -183,7 +183,7 @@ struct LoginFragment: View { .padding(.bottom) NavigationLink(isActive: $isLinkSIPActive, destination: { - ThirdPartySipAccountWarningFragment(sharedMainViewModel: sharedMainViewModel, accountLoginViewModel: accountLoginViewModel) + ThirdPartySipAccountWarningFragment(accountLoginViewModel: accountLoginViewModel) }, label: { Text("Use SIP Account") .default_text_style_orange_600(styleSize: 20) @@ -268,7 +268,7 @@ struct LoginFragment: View { let contentPopup3 = Text(" et ") let contentPopup4 = Text("[nos conditions d’utilisation](https://linphone.org/general-terms)").underline() let contentPopup5 = Text(".") - PopupView(sharedMainViewModel: sharedMainViewModel, isShowPopup: $isShowPopup, + PopupView(isShowPopup: $isShowPopup, title: Text("Conditions de service"), content: contentPopup1 + contentPopup2 + contentPopup3 + contentPopup4 + contentPopup5, titleFirstButton: Text("Deny all"), @@ -283,7 +283,7 @@ struct LoginFragment: View { } if coreContext.loggingInProgress { - PopupLoadingView(sharedMainViewModel: sharedMainViewModel) + PopupLoadingView() .background(.black.opacity(0.65)) } } @@ -306,5 +306,5 @@ struct LoginFragment: View { } #Preview { - LoginFragment(accountLoginViewModel: AccountLoginViewModel(), sharedMainViewModel: SharedMainViewModel()) + LoginFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift index efb732cd4..fb7eed70a 100644 --- a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift +++ b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct PermissionsFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var permissionManager = PermissionManager.shared @@ -172,8 +172,7 @@ struct PermissionsFragment: View { .padding(.horizontal) Button { - permissionManager.contactsRequestPermission() - permissionManager.cameraRequestPermission() + permissionManager.getPermissions() } label: { Text("D'accord") .default_text_style_white_600(styleSize: 20) @@ -193,7 +192,7 @@ struct PermissionsFragment: View { } .navigationViewStyle(StackNavigationViewStyle()) .navigationBarHidden(true) - .onReceive(permissionManager.$cameraPermissionGranted, perform: { (granted) in + .onReceive(permissionManager.$contactsPermissionGranted, perform: { (granted) in if granted { withAnimation { sharedMainViewModel.changeWelcomeView() @@ -204,5 +203,5 @@ struct PermissionsFragment: View { } #Preview { - PermissionsFragment(sharedMainViewModel: SharedMainViewModel()) + PermissionsFragment() } diff --git a/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift b/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift index 0eec8a5e0..fbe34e463 100644 --- a/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct ProfileModeFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @State var options: Int = 1 @State private var isShowPopup = false @@ -142,7 +142,7 @@ struct ProfileModeFragment: View { } if self.isShowPopup { - PopupView(sharedMainViewModel: sharedMainViewModel, isShowPopup: $isShowPopup, + PopupView(isShowPopup: $isShowPopup, title: Text(isShowPopupForDefault ? "Default mode" : "Interoperable mode"), content: Text( isShowPopupForDefault @@ -167,5 +167,5 @@ struct ProfileModeFragment: View { } #Preview { - ProfileModeFragment(sharedMainViewModel: SharedMainViewModel()) + ProfileModeFragment() } diff --git a/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift b/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift index d5c9ad36e..a12a98419 100644 --- a/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift +++ b/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift @@ -22,6 +22,7 @@ import SwiftUI struct QrCodeScannerFragment: View { @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @Environment(\.dismiss) var dismiss @@ -54,13 +55,15 @@ struct QrCodeScannerFragment: View { .edgesIgnoringSafeArea(.all) .navigationBarHidden(true) - if coreContext.toastMessage == "Successful" { + /* + if $isShowToast { ZStack { }.onAppear { dismiss() } } + */ } } diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift index b8be41481..6bb7e3bd0 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct ThirdPartySipAccountLoginFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject private var coreContext = CoreContext.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel @@ -233,5 +233,5 @@ struct ThirdPartySipAccountLoginFragment: View { } #Preview { - ThirdPartySipAccountLoginFragment(sharedMainViewModel: SharedMainViewModel(), accountLoginViewModel: AccountLoginViewModel()) + ThirdPartySipAccountLoginFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift index 3d85dcd02..a3aa14ad5 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct ThirdPartySipAccountWarningFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject private var coreContext = CoreContext.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel @@ -152,7 +152,7 @@ struct ThirdPartySipAccountWarningFragment: View { .padding(.horizontal) NavigationLink(destination: { - ThirdPartySipAccountLoginFragment(sharedMainViewModel: sharedMainViewModel, accountLoginViewModel: accountLoginViewModel) + ThirdPartySipAccountLoginFragment(accountLoginViewModel: accountLoginViewModel) }, label: { Text("I understand") .default_text_style_white_600(styleSize: 20) @@ -178,5 +178,5 @@ struct ThirdPartySipAccountWarningFragment: View { } #Preview { - ThirdPartySipAccountWarningFragment(sharedMainViewModel: SharedMainViewModel(), accountLoginViewModel: AccountLoginViewModel()) + ThirdPartySipAccountWarningFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index 7462e16dd..e9150c82c 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -35,6 +35,17 @@ class AccountLoginViewModel: ObservableObject { func login() { coreContext.doOnCoreQueue { core in do { + + if self.domain != "sip.linphone.org" { + if let assistantLinphone = Bundle.main.path(forResource: "assistant_third_party_default_values", ofType: nil) { + core.loadConfigFromXml(xmlUri: assistantLinphone) + } + } else { + if let assistantLinphone = Bundle.main.path(forResource: "assistant_linphone_default_values", ofType: nil) { + core.loadConfigFromXml(xmlUri: assistantLinphone) + } + } + // Get the transport protocol to use. // TLS is strongly recommended // Only use UDP if you don't have the choice diff --git a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift index 5d546ef96..014cf08f3 100644 --- a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift +++ b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift @@ -43,6 +43,7 @@ struct QRScanner: UIViewControllerRepresentable { class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { private var coreContext = CoreContext.shared + private var sharedMainViewModel = SharedMainViewModel.shared @Binding var scanResult: String private var lastResult: String = "" @@ -76,10 +77,12 @@ class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { try? core.start() } } else { - coreContext.toastMessage = "Invalide URI" + ToastViewModel.shared.toastMessage = "Invalide URI" + ToastViewModel.shared.displayToast.toggle() } } else { - coreContext.toastMessage = "Invalide URI" + ToastViewModel.shared.toastMessage = "Invalide URI" + ToastViewModel.shared.displayToast.toggle() } } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index f1f427e2f..e29473d97 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -34,9 +34,11 @@ struct ContactFragment: View { @State private var showShareSheet = false var body: some View { + let indexDisplayed = contactViewModel.indexDisplayedFriend != nil ? contactViewModel.indexDisplayedFriend! : 0 if #available(iOS 16.0, *) { if idiom != .pad { ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, cnContact: CNContact(), @@ -50,12 +52,13 @@ struct ContactFragment: View { .presentationDetents([.fraction(0.2)]) } .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) .presentationDetents([.medium]) .edgesIgnoringSafeArea(.bottom) } } else { ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, cnContact: CNContact(), @@ -68,12 +71,13 @@ struct ContactFragment: View { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) } onDismiss: {} .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) .edgesIgnoringSafeArea(.bottom) } } } else { ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, cnContact: CNContact(), @@ -86,7 +90,7 @@ struct ContactFragment: View { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) } onDismiss: {} .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) .edgesIgnoringSafeArea(.bottom) } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index ff8e3e2e7..99333cd53 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -1,15 +1,28 @@ -// -// ContactInnerActionsFragment.swift -// Linphone -// -// Created by Benoît Martins on 09/11/2023. -// +/* + * 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 ContactInnerActionsFragment: View { - @ObservedObject var magicSearch = MagicSearchSingleton.shared + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel @@ -47,8 +60,8 @@ struct ContactInnerActionsFragment: View { if informationIsOpen { VStack(spacing: 0) { - if contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { - ForEach(0... + */ + +import Foundation +import linphonesw + +class ContactAvatarModel: ObservableObject { + + let friend: Friend? + + let withPresence: Bool? + + @Published var lastPresenceInfo: String + + @Published var presenceStatus: ConsolidatedPresence + + private var friendDelegate: FriendDelegate? + + init(friend: Friend?, withPresence: Bool?) { + self.friend = friend + self.withPresence = withPresence + if friend != nil && + withPresence == true { + self.lastPresenceInfo = "" + + self.presenceStatus = friend!.consolidatedPresence + + if friend!.consolidatedPresence == .Online || friend!.consolidatedPresence == .Busy { + if friend!.consolidatedPresence == .Online || friend!.presenceModel!.latestActivityTimestamp != -1 { + self.lastPresenceInfo = friend!.consolidatedPresence == .Online ? "Online" : getCallTime(startDate: friend!.presenceModel!.latestActivityTimestamp) + } else { + self.lastPresenceInfo = "Away" + } + } else { + self.lastPresenceInfo = "" + } + + if self.friendDelegate != nil { + self.friend!.removeDelegate(delegate: self.friendDelegate!) + self.friendDelegate = nil + } + + addDelegate() + } else { + self.lastPresenceInfo = "" + self.presenceStatus = .Offline + } + } + + func addDelegate() { + let newFriendDelegate = FriendDelegateStub( + onPresenceReceived: { (linphoneFriend: Friend) -> Void in + DispatchQueue.main.sync { + self.presenceStatus = linphoneFriend.consolidatedPresence + if linphoneFriend.consolidatedPresence == .Online || linphoneFriend.consolidatedPresence == .Busy { + if linphoneFriend.consolidatedPresence == .Online || linphoneFriend.presenceModel!.latestActivityTimestamp != -1 { + self.lastPresenceInfo = linphoneFriend.consolidatedPresence == .Online ? "Online" : self.getCallTime(startDate: linphoneFriend.presenceModel!.latestActivityTimestamp) + } else { + self.lastPresenceInfo = "Away" + } + } else { + self.lastPresenceInfo = "" + } + } + } + ) + + friendDelegate = newFriendDelegate + if friendDelegate != nil { + friend!.addDelegate(delegate: friendDelegate!) + } + } + + func removeAllDelegate() { + if friendDelegate != nil { + presenceStatus = .Offline + friend!.removeDelegate(delegate: friendDelegate!) + friendDelegate = nil + } + } + + func getCallTime(startDate: time_t) -> String { + let timeInterval = TimeInterval(startDate) + + let myNSDate = Date(timeIntervalSince1970: timeInterval) + + if Calendar.current.isDateInToday(myNSDate) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" + return "Online today at " + formatter.string(from: myNSDate) + } else if Calendar.current.isDateInYesterday(myNSDate) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" + return "Online yesterday at " + formatter.string(from: myNSDate) + } else if Calendar.current.isDate(myNSDate, equalTo: .now, toGranularity: .year) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM | HH:mm" : "MM/dd | h:mm a" + return "Online on " + formatter.string(from: myNSDate) + } else { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM/yy | HH:mm" : "MM/dd/yy | h:mm a" + return "Online on " + formatter.string(from: myNSDate) + } + } +} diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift index a3f8443c7..1c17f540c 100644 --- a/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift @@ -29,9 +29,5 @@ class ContactViewModel: ObservableObject { var selectedFriendToShare: Friend? var selectedFriendToDelete: Friend? - private var magicSearch = MagicSearchSingleton.shared - - init() { - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)} + init() {} } diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift index a126f5975..3c59661d2 100644 --- a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift @@ -20,6 +20,6 @@ import linphonesw class ContactsListViewModel: ObservableObject { - + init() {} } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index d9a8307c1..b826ebd73 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -17,18 +17,26 @@ * along with this program. If not, see . */ +// swiftlint:disable type_body_length import SwiftUI import linphonesw struct ContentView: View { - var contactManager = ContactsManager.shared + @Environment(\.scenePhase) var scenePhase + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + + @ObservedObject var contactsManager = ContactsManager.shared var magicSearch = MagicSearchSingleton.shared @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel @ObservedObject var historyViewModel: HistoryViewModel - @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var startCallViewModel: StartCallViewModel @State var index = 0 @State private var orientation = UIDevice.current.orientation @@ -37,9 +45,12 @@ struct ContentView: View { @State private var searchIsActive = false @State private var text = "" @FocusState private var focusedField: Bool + @State private var showingDialer = false @State var isMenuOpen = false - @State var isShowDeletePopup = false + @State var isShowDeleteContactPopup = false + @State var isShowDeleteAllHistoryPopup = false @State var isShowEditContactFragment = false + @State var isShowStartCallFragment = false @State var isShowDismissPopup = false var body: some View { @@ -55,6 +66,7 @@ struct ContentView: View { Spacer() Button(action: { self.index = 0 + historyViewModel.displayedCall = nil }, label: { VStack { Image("address-book") @@ -134,34 +146,49 @@ struct ContentView: View { } 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") - .resizable() - .frame(width: 25, height: 25, alignment: .leading) + if index == 0 { + Button { + isMenuOpen = false + magicSearch.allContact = true + MagicSearchSingleton.shared.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + HStack { + Text("See all") + Spacer() + if magicSearch.allContact { + Image("green-check") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } } } - } - - 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") + + Button { + isMenuOpen = false + magicSearch.allContact = false + MagicSearchSingleton.shared.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + HStack { + Text("See Linphone contact") + Spacer() + if !magicSearch.allContact { + Image("green-check") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + } + } else { + Button(role: .destructive) { + isMenuOpen = false + isShowDeleteAllHistoryPopup.toggle() + } label: { + HStack { + Text("Delete all history") + Spacer() + Image("trash-simple-red") .resizable() .frame(width: 25, height: 25, alignment: .leading) } @@ -193,9 +220,14 @@ struct ContentView: View { } text = "" - magicSearch.currentFilter = "" - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + + if index == 0 { + magicSearch.currentFilter = "" + MagicSearchSingleton.shared.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } else { + historyListViewModel.resetFilterCallLogs() + } } label: { Image("caret-left") .renderingMode(.template) @@ -226,9 +258,13 @@ struct ContentView: View { self.focusedField = true } .onChange(of: text) { newValue in - magicSearch.currentFilter = newValue - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + if index == 0 { + magicSearch.currentFilter = newValue + MagicSearchSingleton.shared.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } else { + historyListViewModel.filterCallLogs(filter: text) + } } } else { TextEditor(text: Binding( @@ -252,7 +288,7 @@ struct ContentView: View { } .onChange(of: text) { newValue in magicSearch.currentFilter = newValue - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } } @@ -281,10 +317,18 @@ struct ContentView: View { historyViewModel: historyViewModel, editContactViewModel: editContactViewModel, isShowEditContactFragment: $isShowEditContactFragment, - isShowDeletePopup: $isShowDeletePopup + isShowDeletePopup: $isShowDeleteContactPopup ) } else if self.index == 1 { - HistoryView() + HistoryView( + historyListViewModel: historyListViewModel, + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + index: $index, + isShowStartCallFragment: $isShowStartCallFragment, + isShowEditContactFragment: $isShowEditContactFragment + ) } } .frame(maxWidth: @@ -315,6 +359,7 @@ struct ContentView: View { Spacer() Button(action: { self.index = 0 + historyViewModel.displayedCall = nil }, label: { VStack { Image("address-book") @@ -367,7 +412,7 @@ struct ContentView: View { } } - if contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty { + if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil { HStack(spacing: 0) { Spacer() .frame(maxWidth: @@ -381,17 +426,38 @@ struct ContentView: View { ContactFragment( contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, - isShowDeletePopup: $isShowDeletePopup, + isShowDeletePopup: $isShowDeleteContactPopup, isShowDismissPopup: $isShowDismissPopup ) - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) } else if self.index == 1 { - HistoryContactFragment() - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + + let contactAvatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) + && $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() + }) + : ContactAvatarModel(friend: nil, withPresence: false) + + HistoryContactFragment( + contactAvatarModel: contactAvatarModel!, + historyViewModel: historyViewModel, + historyListViewModel: historyListViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup, + isShowEditContactFragment: $isShowEditContactFragment, + indexPage: $index + ) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) } } .onAppear { @@ -430,28 +496,45 @@ struct ContentView: View { if isShowEditContactFragment { EditContactFragment( editContactViewModel: editContactViewModel, + contactViewModel: contactViewModel, isShowEditContactFragment: $isShowEditContactFragment, isShowDismissPopup: $isShowDismissPopup ) - .zIndex(3) - .transition(.move(edge: .bottom)) - .onAppear { - contactViewModel.indexDisplayedFriend = nil - } + .zIndex(3) + .transition(.move(edge: .bottom)) + .onAppear { + contactViewModel.indexDisplayedFriend = nil + } } - if isShowDeletePopup { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeletePopup, + if isShowStartCallFragment { + StartCallFragment( + startCallViewModel: startCallViewModel, + isShowStartCallFragment: $isShowStartCallFragment, + showingDialer: $showingDialer + ) + .zIndex(3) + .transition(.move(edge: .bottom)) + .halfSheet(showSheet: $showingDialer) { + DialerBottomSheet( + startCallViewModel: startCallViewModel, + showingDialer: $showingDialer + ) + } onDismiss: {} + } + + if isShowDeleteContactPopup { + PopupView(isShowPopup: $isShowDeleteContactPopup, title: Text( - contactViewModel.selectedFriend != nil - ? "Delete \(contactViewModel.selectedFriend!.name!)?" - : (contactViewModel.indexDisplayedFriend != nil - ? "Delete \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?" - : "Error Name")), + contactViewModel.selectedFriend != nil + ? "Delete \(contactViewModel.selectedFriend!.name!)?" + : (contactViewModel.indexDisplayedFriend != nil + ? "Delete \(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?" + : "Error Name")), content: Text("This contact will be deleted definitively."), titleFirstButton: Text("Cancel"), actionFirstButton: { - self.isShowDeletePopup.toggle()}, + self.isShowDeleteContactPopup.toggle()}, titleSecondButton: Text("Ok"), actionSecondButton: { if contactViewModel.selectedFriendToDelete != nil { @@ -466,24 +549,49 @@ struct ContentView: View { withAnimation { contactViewModel.indexDisplayedFriend = nil } - magicSearch.lastSearch[tmpIndex!].friend!.remove() + contactsManager.lastSearch[tmpIndex!].friend!.remove() } - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) - self.isShowDeletePopup.toggle() + self.isShowDeleteContactPopup.toggle() }) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { - self.isShowDeletePopup.toggle() + self.isShowDeleteContactPopup.toggle() } .onAppear { contactViewModel.selectedFriendToDelete = contactViewModel.selectedFriend } } + if isShowDeleteAllHistoryPopup { + PopupView(isShowPopup: $isShowDeleteContactPopup, + title: Text("Do you really want to delete all calls history?"), + content: Text("All calls will be removed from the history."), + titleFirstButton: Text("Cancel"), + actionFirstButton: { + self.isShowDeleteAllHistoryPopup.toggle() + historyListViewModel.callLogsAddressToDelete = "" + }, + titleSecondButton: Text("Ok"), + actionSecondButton: { + historyListViewModel.removeCallLogs() + self.isShowDeleteAllHistoryPopup.toggle() + historyViewModel.displayedCall = nil + + ToastViewModel.shared.toastMessage = "Success_remove_call_logs" + ToastViewModel.shared.displayToast.toggle() + }) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowDeleteAllHistoryPopup.toggle() + } + } + if isShowDismissPopup { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDismissPopup, + PopupView(isShowPopup: $isShowDismissPopup, title: Text("Don’t save modifications?"), content: Text("All modifications will be canceled."), titleFirstButton: Text("Cancel"), @@ -511,6 +619,11 @@ struct ContentView: View { self.isShowDismissPopup.toggle() } } + + //if sharedMainViewModel.displayToast { + ToastView() + .zIndex(3) + //} } } .overlay { @@ -524,13 +637,27 @@ struct ContentView: View { } } .onRotate { newOrientation in - if (contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive { + if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil) && searchIsActive { self.focusedField = false } else if searchIsActive { self.focusedField = true } orientation = newOrientation } + .onChange(of: scenePhase) { newPhase in + if newPhase == .active { + coreContext.onForeground() + if !isShowStartCallFragment { + contactsManager.fetchContacts() + } + print("Active") + } else if newPhase == .inactive { + print("Inactive") + } else if newPhase == .background { + coreContext.onBackground() + print("Background") + } + } } func openMenu() { @@ -541,5 +668,12 @@ struct ContentView: View { } #Preview { - ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel()) + ContentView( + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + historyViewModel: HistoryViewModel(), + historyListViewModel: HistoryListViewModel(), + startCallViewModel: StartCallViewModel() + ) } +// swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Fragments/PopupLoadingView.swift b/Linphone/UI/Main/Fragments/PopupLoadingView.swift index 99f4f063e..0e1eb2aa6 100644 --- a/Linphone/UI/Main/Fragments/PopupLoadingView.swift +++ b/Linphone/UI/Main/Fragments/PopupLoadingView.swift @@ -21,7 +21,7 @@ import SwiftUI struct PopupLoadingView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var body: some View { GeometryReader { geometry in @@ -54,6 +54,6 @@ struct PopupLoadingView: View { } #Preview { - PopupLoadingView(sharedMainViewModel: SharedMainViewModel()) + PopupLoadingView() .background(.black.opacity(0.65)) } diff --git a/Linphone/UI/Main/Fragments/PopupView.swift b/Linphone/UI/Main/Fragments/PopupView.swift index dbfd53afa..ad4bb3dfd 100644 --- a/Linphone/UI/Main/Fragments/PopupView.swift +++ b/Linphone/UI/Main/Fragments/PopupView.swift @@ -22,7 +22,7 @@ import Photos struct PopupView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var permissionManager = PermissionManager.shared @@ -100,7 +100,7 @@ struct PopupView: View { } #Preview { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: .constant(true), + PopupView(isShowPopup: .constant(true), title: Text("Title"), content: Text("Content"), titleFirstButton: Text("Deny all"), diff --git a/Linphone/UI/Main/Fragments/SideMenu.swift b/Linphone/UI/Main/Fragments/SideMenu.swift index 317490553..60f3c9606 100644 --- a/Linphone/UI/Main/Fragments/SideMenu.swift +++ b/Linphone/UI/Main/Fragments/SideMenu.swift @@ -18,8 +18,15 @@ */ import SwiftUI +import linphonesw +import UniformTypeIdentifiers struct SideMenu: View { + + @ObservedObject private var coreContext = CoreContext.shared + + @State private var coreDelegate: CoreDelegate? + let width: CGFloat let isOpen: Bool let menuClose: () -> Void @@ -41,9 +48,13 @@ struct SideMenu: View { Text("My Profile").onTapGesture { print("My Profile") } - Text("Posts").onTapGesture { - print("Posts") + Text("Send logs").onTapGesture { + sendLogs() } + Text("Clear logs").onTapGesture { + print("Clear logs") + Core.resetLogCollection() + } Text("Logout").onTapGesture { print("Logout") } @@ -60,4 +71,41 @@ struct SideMenu: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) } + + func sendLogs() { + coreContext.doOnCoreQueue { core in + core.uploadLogCollection() + + let newCoreDelegate = CoreDelegateStub( + onLogCollectionUploadStateChanged: { core, logCollectionUploadState, logString in + + if logString.starts(with: "https") { + UIPasteboard.general.setValue( + logString, + forPasteboardType: UTType.plainText.identifier + ) + + removeAllDelegate() + + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() + } + } + ) + + coreDelegate = newCoreDelegate + if coreDelegate != nil { + core.addDelegate(delegate: coreDelegate!) + } + } + } + + func removeAllDelegate() { + coreContext.doOnCoreQueue { core in + if coreDelegate != nil { + core.removeDelegate(delegate: coreDelegate!) + coreDelegate = nil + } + } + } } diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index b24ba953e..40d5e7e16 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -19,30 +19,21 @@ import SwiftUI -struct ToastView: ViewModifier { +struct ToastView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var toastViewModel = ToastViewModel.shared - @Binding var isShowing: String - - func body(content: Content) -> some View { - ZStack { - content - toastView - } - } - - private var toastView: some View { + var body: some View { VStack { - if !isShowing.isEmpty { + if toastViewModel.displayToast { HStack { - Image(isShowing == "Successful" ? "smiley" : "warning-circle") + Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle") .resizable() .renderingMode(.template) .frame(width: 25, height: 25, alignment: .leading) - .foregroundStyle(isShowing == "Successful" ? Color.greenSuccess500 : Color.redDanger500) + .foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500) - switch isShowing { + switch toastViewModel.toastMessage { case "Successful": Text("QR code validated!") .multilineTextAlignment(.center) @@ -50,6 +41,20 @@ struct ToastView: ViewModifier { .default_text_style(styleSize: 15) .padding(8) + case "Success_remove_call_logs": + Text("History has been deleted") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Success_copied_into_clipboard": + Text("SIP address copied into clipboard") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + case "Failed": Text("Invalid QR code!") .multilineTextAlignment(.center) @@ -85,29 +90,30 @@ struct ToastView: ViewModifier { .overlay( RoundedRectangle(cornerRadius: 50) .inset(by: 0.5) - .stroke(isShowing == "Successful" ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) + .stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) ) .onTapGesture { - isShowing = "" + withAnimation { + toastViewModel.toastMessage = "" + toastViewModel.displayToast = false + } } .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - isShowing = "" + withAnimation { + toastViewModel.toastMessage = "" + toastViewModel.displayToast = false + } } } + + Spacer() } - Spacer() } - .frame(maxWidth: sharedMainViewModel.maxWidth) + .frame(maxWidth: SharedMainViewModel.shared.maxWidth) .padding(.horizontal, 16) .padding(.bottom, 18) - .animation(.linear(duration: 0.3), value: isShowing) - .transition(.opacity) - } -} - -extension View { - func toast(isShowing: Binding) -> some View { - self.modifier(ToastView(sharedMainViewModel: SharedMainViewModel(), isShowing: isShowing)) + .transition(.move(edge: .top)) + .padding(.top, 60) } } diff --git a/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift b/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift new file mode 100644 index 000000000..f1737dc09 --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift @@ -0,0 +1,320 @@ +/* + * 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 UniformTypeIdentifiers + +struct DialerBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject private var magicSearch = MagicSearchSingleton.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var startCallViewModel: StartCallViewModel + + @State private var orientation = UIDevice.current.orientation + + @Binding var showingDialer: Bool + + var body: some View { + VStack(alignment: .center, spacing: 0) { + VStack(alignment: .center, spacing: 0) { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("Close") { + showingDialer.toggle() + dismiss() + } + } + .padding(.trailing) + } else { + Capsule() + .fill(Color.grayMain2c300) + .frame(width: 75, height: 5) + .padding(15) + } + + Spacer() + + HStack { + Button { + startCallViewModel.searchField += "1" + } label: { + Text("1") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "2" + } label: { + Text("2") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "3" + } label: { + Text("3") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "4" + } label: { + Text("4") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "5" + } label: { + Text("5") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "6" + } label: { + Text("6") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "7" + } label: { + Text("7") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "8" + } label: { + Text("8") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "9" + } label: { + Text("9") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "*" + } label: { + Text("*") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + } label: { + ZStack { + Text("0") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 75) + .padding(.top, -15) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + Text("+") + .default_text_style(styleSize: 20) + .multilineTextAlignment(.center) + .frame(width: 60, height: 85) + .padding(.bottom, -25) + .background(.clear) + .clipShape(Circle()) + } + } + .simultaneousGesture( + LongPressGesture() + .onEnded { _ in + startCallViewModel.searchField += "+" + } + ) + .highPriorityGesture( + TapGesture() + .onEnded { _ in + startCallViewModel.searchField += "0" + } + ) + + Spacer() + + Button { + startCallViewModel.searchField += "#" + } label: { + Text("#") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + + HStack { + + } + .frame(width: 60, height: 60) + + Spacer() + + Button { + } label: { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 90, height: 60) + .background(Color.greenSuccess500) + .cornerRadius(40) + .shadow(color: .black.opacity(0.2), radius: 4) + + Spacer() + + Button { + startCallViewModel.searchField = String(startCallViewModel.searchField.dropLast()) + } label: { + Image("backspace-fill") + .resizable() + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + } + .padding(.horizontal, 60) + .padding(.top, 20) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + .onRotate { newOrientation in + orientation = newOrientation + } + } +} + +#Preview { + DialerBottomSheet( + startCallViewModel: StartCallViewModel(), showingDialer: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index ef7dc3419..dbb5c4a4a 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -18,17 +18,529 @@ */ import SwiftUI +import UniformTypeIdentifiers struct HistoryContactFragment: View { - var body: some View { - VStack { - Spacer() - Text("History Contact fragment") - Spacer() + + @State private var orientation = UIDevice.current.orientation + + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var contactAvatarModel: ContactAvatarModel + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + + @State var isMenuOpen = false + + @Binding var isShowDeleteAllHistoryPopup: Bool + @Binding var isShowEditContactFragment: Bool + @Binding var indexPage: Int + + var body: some View { + 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 { + historyViewModel.displayedCall = nil + } + } + } + + Text("Call history") + .default_text_style_orange_800(styleSize: 20) + + Spacer() + + Menu { + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + + Button { + isMenuOpen = false + + if contactsManager.getFriendWithAddress( + address: historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + ) != nil { + let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + + let friendIndex = contactsManager.lastSearch.firstIndex( + where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) + if friendIndex != nil { + + withAnimation { + historyViewModel.displayedCall = nil + indexPage = 0 + + contactViewModel.indexDisplayedFriend = friendIndex + } + } + } else { + let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + + withAnimation { + historyViewModel.displayedCall = nil + indexPage = 0 + + isShowEditContactFragment.toggle() + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) + editContactViewModel.sipAddresses.append("") + } + } + + } label: { + HStack { + Text(addressFriend != nil ? "See contact" : "Add to contacts") + Spacer() + Image(addressFriend != nil ? "user-circle" : "plus-circle") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + + Button { + isMenuOpen = false + + if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing { + UIPasteboard.general.setValue( + historyViewModel.displayedCall!.toAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } else { + UIPasteboard.general.setValue( + historyViewModel.displayedCall!.fromAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } + + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() + + } label: { + HStack { + Text("Copy SIP address") + Spacer() + Image("copy") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + + Button(role: .destructive) { + isMenuOpen = false + + if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing { + historyListViewModel.callLogsAddressToDelete = historyViewModel.displayedCall!.toAddress!.asStringUriOnly() + } else { + historyListViewModel.callLogsAddressToDelete = historyViewModel.displayedCall!.fromAddress!.asStringUriOnly() + } + + isShowDeleteAllHistoryPopup.toggle() + + } label: { + HStack { + Text("Delete history") + Spacer() + Image("trash-simple-red") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + } label: { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + } + .padding(.leading) + .onTapGesture { + isMenuOpen = true + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + VStack(spacing: 0) { + VStack(spacing: 0) { + + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + + if historyViewModel.displayedCall != nil + && addressFriend != nil + && addressFriend!.photo != nil + && !addressFriend!.photo!.isEmpty { + Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100) + } else if historyViewModel.displayedCall != nil { + if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { + if historyViewModel.displayedCall!.toAddress!.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: historyViewModel.displayedCall!.toAddress!.displayName!, + lastName: historyViewModel.displayedCall!.toAddress!.displayName!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.toAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.toAddress!.displayName!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } else { + Image(uiImage: contactsManager.textToImage( + firstName: historyViewModel.displayedCall!.toAddress!.username ?? "Username Error", + lastName: historyViewModel.displayedCall!.toAddress!.username!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.toAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.toAddress!.username ?? "Username Error") + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } + + } else if historyViewModel.displayedCall!.fromAddress != nil { + if historyViewModel.displayedCall!.fromAddress!.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: historyViewModel.displayedCall!.fromAddress!.displayName!, + lastName: historyViewModel.displayedCall!.fromAddress!.displayName!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.fromAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.fromAddress!.displayName!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } else { + Image(uiImage: contactsManager.textToImage( + firstName: historyViewModel.displayedCall!.fromAddress!.username ?? "Username Error", + lastName: historyViewModel.displayedCall!.fromAddress!.username!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.fromAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.fromAddress!.username ?? "Username Error") + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } + } + } + if historyViewModel.displayedCall != nil + && addressFriend != nil + && addressFriend!.name != nil { + Text((addressFriend!.name)!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + } else if historyViewModel.displayedCall!.fromAddress != nil { + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + } + + Text(contactAvatarModel.lastPresenceInfo) + .foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online" + ? Color.greenSuccess500 + : Color.orangeWarning600) + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + .padding(.top, 5) + } + } + .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 { + + } + } + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("Appel") + .default_text_style(styleSize: 14) + } + }) + + Spacer() + + Button(action: { + + }, 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) + + 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) + + VStack(spacing: 0) { + + let addressFriend = historyViewModel.displayedCall != nil + ? (historyViewModel.displayedCall!.dir == .Incoming ? historyViewModel.displayedCall!.fromAddress!.asStringUriOnly() + : historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) : nil + + let callLogsFilter = historyListViewModel.callLogs.filter({ $0.dir == .Incoming + ? $0.fromAddress!.asStringUriOnly() == addressFriend + : $0.toAddress!.asStringUriOnly() == addressFriend }) + + ForEach(0... + */ + +import SwiftUI + +struct HistoryFragment: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + + @State private var showingSheet = false + @Binding var index: Int + @Binding var isShowEditContactFragment: Bool + + var body: some View { + ZStack { + if #available(iOS 16.0, *) { + if idiom != .pad { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .sheet(isPresented: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + .presentationDetents([.fraction(0.2)]) + } + } else { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .halfSheet(showSheet: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + } onDismiss: {} + } + } else { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .halfSheet(showSheet: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + } onDismiss: {} + } + } + } +} + +#Preview { + HistoryFragment( + historyListViewModel: HistoryListViewModel(), + historyViewModel: HistoryViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + index: .constant(1), + isShowEditContactFragment: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift new file mode 100644 index 000000000..fb8dfd731 --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift @@ -0,0 +1,247 @@ +/* + * 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 UniformTypeIdentifiers + +struct HistoryListBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var historyListViewModel: HistoryListViewModel + + @State private var orientation = UIDevice.current.orientation + + @Binding var showingSheet: Bool + @Binding var index: Int + @Binding var isShowEditContactFragment: 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 #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + + index = 0 + + if contactsManager.getFriendWithAddress( + address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + ) != nil { + let addressCall = historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + + let friendIndex = contactsManager.lastSearch.firstIndex(where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) + if friendIndex != nil { + withAnimation { + contactViewModel.indexDisplayedFriend = friendIndex + } + } + } else { + let addressCall = historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + + withAnimation { + isShowEditContactFragment.toggle() + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) + editContactViewModel.sipAddresses.append("") + } + } + } label: { + HStack { + if contactsManager.getFriendWithAddress( + address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + ) != nil { + Image("user-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("See contact") + .default_text_style(styleSize: 16) + Spacer() + } else { + Image("plus-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Add the contact") + .default_text_style(styleSize: 16) + Spacer() + } + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + if historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing { + UIPasteboard.general.setValue( + historyViewModel.selectedCall!.toAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } else { + UIPasteboard.general.setValue( + historyViewModel.selectedCall!.fromAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() + + } label: { + HStack { + Image("copy") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Copy SIP address") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + CoreContext.shared.doOnCoreQueue { core in + if historyViewModel.selectedCall != nil { + core.removeCallLog(callLog: historyViewModel.selectedCall!) + historyListViewModel.removeCallLog(callLog: historyViewModel.selectedCall!) + } + } + + 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 + } + } +} + +#Preview { + HistoryListBottomSheet( + historyViewModel: HistoryViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + historyListViewModel: HistoryListViewModel(), + showingSheet: .constant(false), + index: .constant(1), + isShowEditContactFragment: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift new file mode 100644 index 000000000..a2fcac515 --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -0,0 +1,197 @@ +/* + * 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 HistoryListFragment: View { + + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var historyViewModel: HistoryViewModel + + @Binding var showingSheet: Bool + + var body: some View { + VStack { + List { + ForEach(0.. 1 + ? historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + + } else { + Image(uiImage: contactsManager.textToImage( + firstName: historyListViewModel.callLogs[index].toAddress!.username ?? "Username Error", + lastName: historyListViewModel.callLogs[index].toAddress!.username!.components(separatedBy: " ").count > 1 + ? historyListViewModel.callLogs[index].toAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + } + + } else if historyListViewModel.callLogs[index].fromAddress != nil { + if historyListViewModel.callLogs[index].fromAddress!.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: historyListViewModel.callLogs[index].fromAddress!.displayName!, + lastName: historyListViewModel.callLogs[index].fromAddress!.displayName!.components(separatedBy: " ").count > 1 + ? historyListViewModel.callLogs[index].fromAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + } else { + Image(uiImage: contactsManager.textToImage( + firstName: historyListViewModel.callLogs[index].fromAddress!.username ?? "Username Error", + lastName: historyListViewModel.callLogs[index].fromAddress!.username!.components(separatedBy: " ").count > 1 + ? historyListViewModel.callLogs[index].fromAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + } + } + } + + VStack(spacing: 0) { + Spacer() + + let fromAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!) + let toAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!) + let addressFriend = historyListViewModel.callLogs[index].dir == .Incoming ? fromAddressFriend : toAddressFriend + + if addressFriend != nil { + Text(addressFriend!.name!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil { + Text(historyListViewModel.callLogs[index].toAddress!.displayName != nil + ? historyListViewModel.callLogs[index].toAddress!.displayName! + : historyListViewModel.callLogs[index].toAddress!.username!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else if historyListViewModel.callLogs[index].fromAddress != nil { + Text(historyListViewModel.callLogs[index].fromAddress!.displayName != nil + ? historyListViewModel.callLogs[index].fromAddress!.displayName! + : historyListViewModel.callLogs[index].fromAddress!.username!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } + HStack { + Image(historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir)) + .resizable() + .frame( + width: historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir).contains("rejected") ? 12 : 8, + height: historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir).contains("rejected") ? 6 : 8) + + Text(historyListViewModel.getCallTime(startDate: historyListViewModel.callLogs[index].startDate)) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + } + + Spacer() + } + + Image("phone") + .resizable() + .frame(width: 25, height: 25) + .padding(.trailing, 5) + } + } + .buttonStyle(.borderless) + .listRowInsets(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)) + .listRowSeparator(.hidden) + .simultaneousGesture( + LongPressGesture() + .onEnded { _ in + historyViewModel.selectedCall = historyListViewModel.callLogs[index] + showingSheet.toggle() + } + ) + .highPriorityGesture( + TapGesture() + .onEnded { _ in + withAnimation { + historyViewModel.displayedCall = historyListViewModel.callLogs[index] + } + } + ) + } + } + .listStyle(.plain) + .overlay( + VStack { + if historyListViewModel.callLogs.isEmpty { + Spacer() + Image("illus-belledonne") + .resizable() + .scaledToFit() + .clipped() + .padding(.all) + Text("No call for the moment...") + .default_text_style_800(styleSize: 16) + Spacer() + Spacer() + } + } + .padding(.all) + ) + } + } +} + +#Preview { + HistoryListFragment(historyListViewModel: HistoryListViewModel(), historyViewModel: HistoryViewModel(), showingSheet: .constant(false)) +} diff --git a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift new file mode 100644 index 000000000..b03dde24e --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift @@ -0,0 +1,245 @@ +/* + * 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 StartCallFragment: View { + + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var startCallViewModel: StartCallViewModel + + @Binding var isShowStartCallFragment: Bool + @Binding var showingDialer: Bool + + @FocusState var isSearchFieldFocused: Bool + @State private var hasTimeElapsed = false + @State private var delayedColor = Color.white + + var body: some View { + ZStack { + VStack(spacing: 1) { + + Rectangle() + .foregroundColor(delayedColor) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + .task(delayColor) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) + .onTapGesture { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + + startCallViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + delayColorDismiss() + withAnimation { + isShowStartCallFragment.toggle() + } + } + + Text("New call") + .multilineTextAlignment(.leading) + .default_text_style_orange_800(styleSize: 16) + + Spacer() + + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + VStack(spacing: 0) { + ZStack(alignment: .trailing) { + TextField("Search contact or history call", text: $startCallViewModel.searchField) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isSearchFieldFocused) + .padding(.horizontal, 30) + .onChange(of: startCallViewModel.searchField) { newValue in + magicSearch.currentFilterSuggestions = newValue + magicSearch.searchForSuggestions() + } + + HStack { + Button(action: { + }, label: { + Image("magnifying-glass") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + + Spacer() + + if startCallViewModel.searchField.isEmpty { + Button(action: { + isSearchFieldFocused = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + showingDialer.toggle() + } + }, label: { + Image(!showingDialer ? "dialer" : "keyboard") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + } else { + Button(action: { + startCallViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + magicSearch.searchForSuggestions() + }, label: { + Image("x") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + } + } + } + .padding(.horizontal, 15) + .padding(.vertical, 10) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSearchFieldFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.vertical) + .padding(.horizontal) + + ScrollView { + if !ContactsManager.shared.lastSearch.isEmpty { + HStack(alignment: .center) { + Text("All contacts") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + } + + ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false)) + .padding(.horizontal, 16) + + HStack(alignment: .center) { + Text("Suggestions") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } + } + .frame(maxWidth: .infinity) + } + .background(.white) + } + .navigationBarHidden(true) + } + + @Sendable private func delayColor() async { + try? await Task.sleep(nanoseconds: 250_000_000) + delayedColor = Color.orangeMain500 + } + + func delayColorDismiss() { + Task { + try? await Task.sleep(nanoseconds: 80_000_000) + delayedColor = .white + } + } + + var suggestionsList: some View { + ForEach(0... + */ + +import linphonesw + +class HistoryListViewModel: ObservableObject { + + private var coreContext = CoreContext.shared + + @Published var callLogs: [CallLog] = [] + var callLogsTmp: [CallLog] = [] + + @Published private var coreDelegate: CoreDelegate? + + var callLogsAddressToDelete = "" + + init() { + removeAllDelegate() + computeCallLogsList() + } + + func computeCallLogsList() { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs + + DispatchQueue.main.async { + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + + logs.forEach { log in + self.callLogs.append(log) + self.callLogsTmp.append(log) + } + } + + DispatchQueue.main.async { + self.coreDelegate = CoreDelegateStub( + onCallLogUpdated: { (_: Core, _: CallLog) -> Void in + DispatchQueue.main.sync { + let account = core.defaultAccount + let logs = account != nil ? account!.callLogs : core.callLogs + + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + + logs.forEach { log in + self.callLogs.append(log) + self.callLogsTmp.append(log) + } + } + } + ) + if self.coreDelegate != nil { + core.addDelegate(delegate: self.coreDelegate!) + } + } + } + } + + func getCallIconResId(callStatus: Call.Status, callDir: Call.Dir) -> String { + switch callStatus { + case Call.Status.Missed: + if callDir == .Outgoing { + "outgoing-call-missed" + } else { + "incoming-call-missed" + } + + case Call.Status.Success: + if callDir == .Outgoing { + "outgoing-call" + } else { + "incoming-call" + } + + default: + if callDir == .Outgoing { + "outgoing-call-rejected" + } else { + "incoming-call-rejected" + } + } + } + + func getCallText(callStatus: Call.Status, callDir: Call.Dir) -> String { + switch callStatus { + case Call.Status.Missed: + if callDir == .Outgoing { + "Outgoing Call" + } else { + "Missed Call" + } + + case Call.Status.Success: + if callDir == .Outgoing { + "Outgoing Call" + } else { + "Incoming Call" + } + + default: + if callDir == .Outgoing { + "Outgoing Call" + } else { + "Incoming Call" + } + } + } + + func getCallTime(startDate: time_t) -> String { + let timeInterval = TimeInterval(startDate) + + let myNSDate = Date(timeIntervalSince1970: timeInterval) + + if Calendar.current.isDateInToday(myNSDate) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" + return "Today | " + formatter.string(from: myNSDate) + } else if Calendar.current.isDateInYesterday(myNSDate) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" + return "Yesterday | " + formatter.string(from: myNSDate) + } else if Calendar.current.isDate(myNSDate, equalTo: .now, toGranularity: .year) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM | HH:mm" : "MM/dd | h:mm a" + return formatter.string(from: myNSDate) + } else { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM/yy | HH:mm" : "MM/dd/yy | h:mm a" + return formatter.string(from: myNSDate) + } + } + + func filterCallLogs(filter: String) { + callLogs.removeAll() + callLogsTmp.forEach { callLog in + if callLog.dir == .Outgoing && callLog.toAddress != nil { + if callLog.toAddress!.username != nil && callLog.toAddress!.username!.contains(filter) { + callLogs.append(callLog) + } else if callLog.toAddress!.displayName != nil && callLog.toAddress!.displayName!.contains(filter) { + callLogs.append(callLog) + } + } else if callLog.fromAddress != nil { + if callLog.fromAddress!.username != nil && callLog.fromAddress!.username!.contains(filter) { + callLogs.append(callLog) + } else if callLog.fromAddress!.displayName != nil && callLog.fromAddress!.displayName!.contains(filter) { + callLogs.append(callLog) + } + } + } + } + + func resetFilterCallLogs() { + callLogs = callLogsTmp + } + + func removeCallLogs() { + if callLogsAddressToDelete.isEmpty { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + if account != nil { + account!.clearCallLogs() + } else { + core.clearCallLogs() + } + + DispatchQueue.main.async { + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + } + } + } else { + removeCallLogsWithAddress() + callLogsAddressToDelete = "" + } + } + + func removeCallLogsWithAddress() { + self.callLogs.filter { $0.toAddress!.asStringUriOnly() == callLogsAddressToDelete || $0.fromAddress!.asStringUriOnly() == callLogsAddressToDelete }.forEach { callLog in + removeCallLog(callLog: callLog) + + coreContext.doOnCoreQueue { core in + core.removeCallLog(callLog: callLog) + } + } + } + + func removeCallLog(callLog: CallLog) { + let index = self.callLogs.firstIndex(where: {$0.callId == callLog.callId}) + self.callLogs.remove(at: index!) + + let indexTmp = self.callLogsTmp.firstIndex(where: {$0.callId == callLog.callId}) + self.callLogsTmp.remove(at: indexTmp!) + } + + func removeAllDelegate() { + coreContext.doOnCoreQueue { core in + if self.coreDelegate != nil { + core.removeDelegate(delegate: self.coreDelegate!) + self.coreDelegate = nil + } + } + } + +} diff --git a/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift index 3222b6a0f..02425274d 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift @@ -18,10 +18,13 @@ */ import Foundation +import linphonesw class HistoryViewModel: ObservableObject { - @Published var historyTitle: String = "" + @Published var displayedCall: CallLog? + + var selectedCall: CallLog? init() {} } diff --git a/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift b/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift new file mode 100644 index 000000000..b213d37ce --- /dev/null +++ b/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift @@ -0,0 +1,27 @@ +/* + * 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 StartCallViewModel: ObservableObject { + + @Published var searchField: String = "" + + init() {} +} diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index 6adcc0dcc..abb48824b 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -21,6 +21,8 @@ import linphonesw class SharedMainViewModel: ObservableObject { + static let shared = SharedMainViewModel() + @Published var welcomeViewDisplayed = false @Published var generalTermsAccepted = false @Published var displayProfileMode = false @@ -31,7 +33,7 @@ class SharedMainViewModel: ObservableObject { var maxWidth = 400.0 - init() { + private init() { let preferences = UserDefaults.standard if preferences.object(forKey: welcomeViewKey) == nil { diff --git a/Linphone/UI/Main/Viewmodel/ToastViewModel.swift b/Linphone/UI/Main/Viewmodel/ToastViewModel.swift new file mode 100644 index 000000000..b046c5a06 --- /dev/null +++ b/Linphone/UI/Main/Viewmodel/ToastViewModel.swift @@ -0,0 +1,31 @@ +/* + * 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 Foundation + +class ToastViewModel: ObservableObject { + + static let shared = ToastViewModel() + + var toastMessage: String = "" + @Published var displayToast = false + + private init() { + } +} diff --git a/Linphone/UI/Welcome/WelcomeView.swift b/Linphone/UI/Welcome/WelcomeView.swift index 5a4d6e700..6e19048a4 100644 --- a/Linphone/UI/Welcome/WelcomeView.swift +++ b/Linphone/UI/Welcome/WelcomeView.swift @@ -21,7 +21,7 @@ import SwiftUI struct WelcomeView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @State private var index = 0 @@ -39,7 +39,7 @@ struct WelcomeView: View { VStack(alignment: .trailing) { NavigationLink(destination: { - PermissionsFragment(sharedMainViewModel: sharedMainViewModel) + PermissionsFragment() }, label: { Text("Skip") .underline() @@ -96,7 +96,7 @@ struct WelcomeView: View { if index == 2 { NavigationLink(destination: { - PermissionsFragment(sharedMainViewModel: sharedMainViewModel) + PermissionsFragment() }, label: { Text("Start") .default_text_style_white_600(styleSize: 20) @@ -158,5 +158,5 @@ struct WelcomeView: View { } #Preview { - WelcomeView(sharedMainViewModel: SharedMainViewModel()) + WelcomeView() } diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift new file mode 100644 index 000000000..609b13885 --- /dev/null +++ b/Linphone/Utils/Avatar.swift @@ -0,0 +1,66 @@ +/* + * 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 Avatar: View { + + @ObservedObject var contactAvatarModel: ContactAvatarModel + let avatarSize: CGFloat + + var body: some View { + AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: contactAvatarModel.friend!.photo!)) { image in + switch image { + case .empty: + ProgressView() + .frame(width: avatarSize, height: avatarSize) + case .success(let image): + ZStack { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + HStack { + Spacer() + VStack { + Spacer() + if contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy { + Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy") + .resizable() + .frame(width: avatarSize/4, height: avatarSize/4) + .padding(.trailing, avatarSize == 45 ? 1 : 3) + .padding(.bottom, avatarSize == 45 ? 1 : 3) + } + } + } + .frame(width: avatarSize, height: avatarSize) + } + case .failure: + Image("profil-picture-default") + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + @unknown default: + EmptyView() + } + } + } +} diff --git a/Linphone/Utils/EditContactController.swift b/Linphone/Utils/EditContactController.swift index b3a9d250e..4734b5562 100644 --- a/Linphone/Utils/EditContactController.swift +++ b/Linphone/Utils/EditContactController.swift @@ -50,10 +50,11 @@ struct EditContactView: UIViewControllerRepresentable { && 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" : ""), + name: cnc.givenName + cnc.familyName, + prefix: ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, - existingFriend: ContactsManager.shared.getFriend(contact: newContact)) + existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact)) MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } diff --git a/Linphone/Utils/IntExtension.swift b/Linphone/Utils/IntExtension.swift new file mode 100644 index 000000000..0f457700a --- /dev/null +++ b/Linphone/Utils/IntExtension.swift @@ -0,0 +1,67 @@ +/* + * 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 Foundation + +extension Int { + + public func hmsFrom() -> (Int, Int, Int) { + return (self / 3600, (self % 3600) / 60, (self % 3600) % 60) + } + + public func convertDurationToString() -> String { + var duration = "" + let (hour, minute, second) = self.hmsFrom() + if hour > 0 { + duration = self.getHour(hour: hour) + } + return "\(duration)\(self.getMinute(minute: minute))\(self.getSecond(second: second))" + } + + private func getHour(hour: Int) -> String { + var duration = "\(hour):" + if hour < 10 { + duration = "0\(hour):" + } + return duration + } + + private func getMinute(minute: Int) -> String { + if minute == 0 { + return "00:" + } + + if minute < 10 { + return "0\(minute):" + } + + return "\(minute):" + } + + private func getSecond(second: Int) -> String { + if second == 0 { + return "00" + } + + if second < 10 { + return "0\(second)" + } + return "\(second)" + } +} diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 7ce369b0a..0cd349bb8 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -23,15 +23,17 @@ final class MagicSearchSingleton: ObservableObject { static let shared = MagicSearchSingleton() private var coreContext = CoreContext.shared + private var contactsManager = ContactsManager.shared private var magicSearch: MagicSearch! var currentFilter: String = "" var previousFilter: String? - var needUpdateLastSearchContacts = false + var currentFilterSuggestions: String = "" + var previousFilterSuggestions: String? - @Published var lastSearch: [SearchResult] = [] + var needUpdateLastSearchContacts = false private var limitSearchToLinphoneAccounts = true @@ -47,7 +49,39 @@ final class MagicSearchSingleton: ObservableObject { self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in self.needUpdateLastSearchContacts = true - self.lastSearch = magicSearch.lastSearch + + var lastSearchFriend: [SearchResult] = [] + var lastSearchSuggestions: [SearchResult] = [] + + magicSearch.lastSearch.forEach { searchResult in + if searchResult.friend != nil { + lastSearchFriend.append(searchResult) + } else { + lastSearchSuggestions.append(searchResult) + } + } + + self.contactsManager.lastSearch = lastSearchFriend.sorted(by: { + $0.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current) + < + $1.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current) + }) + + self.contactsManager.lastSearchSuggestions = lastSearchSuggestions.sorted(by: { + $0.address!.asStringUriOnly() < $1.address!.asStringUriOnly() + }) + + self.contactsManager.avatarListModel.forEach { contactAvatarModel in + contactAvatarModel.removeAllDelegate() + } + + self.contactsManager.avatarListModel.removeAll() + + self.contactsManager.lastSearch.forEach { searchResult in + if searchResult.friend != nil { + self.contactsManager.avatarListModel.append(ContactAvatarModel(friend: searchResult.friend!, withPresence: true)) + } + } } } } @@ -75,4 +109,28 @@ final class MagicSearchSingleton: ObservableObject { aggregation: MagicSearch.Aggregation.Friend) } } + + func searchForSuggestions() { + coreContext.doOnCoreQueue { _ in + var needResetCache = false + + DispatchQueue.main.sync { + if let oldFilter = self.previousFilterSuggestions { + if oldFilter.count > self.currentFilterSuggestions.count || oldFilter != self.currentFilterSuggestions { + needResetCache = true + } + } + self.previousFilterSuggestions = self.currentFilterSuggestions + } + if needResetCache { + self.magicSearch.resetSearchCache() + } + + self.magicSearch.getContactsListAsync( + filter: self.currentFilterSuggestions, + domain: self.domainDefaultAccount, + sourceFlags: MagicSearch.Source.All.rawValue, + aggregation: MagicSearch.Aggregation.Friend) + } + } } diff --git a/Linphone/Utils/PermissionManager.swift b/Linphone/Utils/PermissionManager.swift index f1d741f7e..e19833012 100644 --- a/Linphone/Utils/PermissionManager.swift +++ b/Linphone/Utils/PermissionManager.swift @@ -31,6 +31,13 @@ class PermissionManager: ObservableObject { private init() {} + + func getPermissions(){ + photoLibraryRequestPermission() + cameraRequestPermission() + contactsRequestPermission() + } + func photoLibraryRequestPermission() { PHPhotoLibrary.requestAuthorization(for: .readWrite, handler: {status in DispatchQueue.main.async {