Merge branch 'feature/start_call_view' into 'master'

Merge tous les changements de benoit : call history, History call details, presence, start call view

See merge request BC/private/linphone-iphone-6.0!12
This commit is contained in:
Quentin Arguillere 2023-12-09 12:27:17 +00:00
commit 1dde8873a8
65 changed files with 3435 additions and 499 deletions

View file

@ -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 = "<group>"; };
D72343332ACEFFC3009AA24E /* QRScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanner.swift; sourceTree = "<group>"; };
D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D726E4382B16440C0083C415 /* ContactAvatarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAvatarModel.swift; sourceTree = "<group>"; };
D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallFragment.swift; sourceTree = "<group>"; };
D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.swift; sourceTree = "<group>"; };
D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = "<group>"; };
D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = "<group>"; };
D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = "<group>"; };
D732A90B2B0376F500DB42BA /* linphonerc-factory */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-factory"; sourceTree = "<group>"; };
D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFragment.swift; sourceTree = "<group>"; };
D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = "<group>"; };
D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = "<group>"; };
D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListBottomSheet.swift; sourceTree = "<group>"; };
D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = "<group>"; };
D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = "<group>"; };
D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = "<group>"; };
@ -103,15 +127,21 @@
D74C9CFE2ACAEC5E0021626A /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = "<group>"; };
D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = "<group>"; };
D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = "<group>"; };
D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = "<group>"; };
D78290B72ADD3910004AA85C /* ContactsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsFragment.swift; sourceTree = "<group>"; };
D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = "<group>"; };
D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = "<group>"; };
D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = "<group>"; };
D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = "<group>"; };
D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = "<group>"; };
D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
D7A03FC52ACC458A0081A588 /* SplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = "<group>"; };
D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedMainViewModel.swift; sourceTree = "<group>"; };
D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = "<group>"; };
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = "<group>"; };
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -201,6 +233,7 @@
D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */,
D719ABBD2ABC67BF00B41C10 /* Preview Content */,
D7D24D0C2AC1B4C700C6F35B /* Fonts */,
D7ADF6012AFE5C7C00212231 /* Ressources */,
);
path = Linphone;
sourceTree = "<group>";
@ -267,14 +300,29 @@
isa = PBXGroup;
children = (
D72250622ADE9615008FB426 /* HistoryViewModel.swift */,
D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */,
D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
D726E4372B1643FF0083C415 /* Model */ = {
isa = PBXGroup;
children = (
D726E4382B16440C0083C415 /* ContactAvatarModel.swift */,
);
path = Model;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
};
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 = "<group>";
};
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 */,

BIN
Linphone/.DS_Store vendored

Binary file not shown.

View file

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

View file

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19.5C10.9 19.5 10 20.4 10 21.5C10 22.6 10.9 23.5 12 23.5C13.1 23.5 14 22.6 14 21.5C14 20.4 13.1 19.5 12 19.5ZM6 1.5C4.9 1.5 4 2.4 4 3.5C4 4.6 4.9 5.5 6 5.5C7.1 5.5 8 4.6 8 3.5C8 2.4 7.1 1.5 6 1.5ZM6 7.5C4.9 7.5 4 8.4 4 9.5C4 10.6 4.9 11.5 6 11.5C7.1 11.5 8 10.6 8 9.5C8 8.4 7.1 7.5 6 7.5ZM6 13.5C4.9 13.5 4 14.4 4 15.5C4 16.6 4.9 17.5 6 17.5C7.1 17.5 8 16.6 8 15.5C8 14.4 7.1 13.5 6 13.5ZM18 5.5C19.1 5.5 20 4.6 20 3.5C20 2.4 19.1 1.5 18 1.5C16.9 1.5 16 2.4 16 3.5C16 4.6 16.9 5.5 18 5.5ZM12 13.5C10.9 13.5 10 14.4 10 15.5C10 16.6 10.9 17.5 12 17.5C13.1 17.5 14 16.6 14 15.5C14 14.4 13.1 13.5 12 13.5ZM18 13.5C16.9 13.5 16 14.4 16 15.5C16 16.6 16.9 17.5 18 17.5C19.1 17.5 20 16.6 20 15.5C20 14.4 19.1 13.5 18 13.5ZM18 7.5C16.9 7.5 16 8.4 16 9.5C16 10.6 16.9 11.5 18 11.5C19.1 11.5 20 10.6 20 9.5C20 8.4 19.1 7.5 18 7.5ZM12 7.5C10.9 7.5 10 8.4 10 9.5C10 10.6 10.9 11.5 12 11.5C13.1 11.5 14 10.6 14 9.5C14 8.4 13.1 7.5 12 7.5ZM12 1.5C10.9 1.5 10 2.4 10 3.5C10 4.6 10.9 5.5 12 5.5C13.1 5.5 14 4.6 14 3.5C14 2.4 13.1 1.5 12 1.5Z" fill="#4E6074"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

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

View file

@ -0,0 +1,5 @@
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Pastille de pr&#195;&#169;sence">
<circle id="Ellipse 2" cx="11.7222" cy="11.8889" r="10.1111" fill="#DBB820" stroke="white" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View file

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

View file

@ -0,0 +1,5 @@
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Pastille de pr&#195;&#169;sence">
<circle id="Ellipse 2" cx="11.7224" cy="11.8889" r="10.1111" fill="#4FAE80" stroke="white" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View file

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

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3333 5.76923H4.66667C4.48986 5.76923 4.32029 5.84217 4.19526 5.972C4.07024 6.10184 4 6.27793 4 6.46154C4 6.64515 4.07024 6.82124 4.19526 6.95107C4.32029 7.08091 4.48986 7.15385 4.66667 7.15385H5.33333V19.6154C5.33333 19.9826 5.47381 20.3348 5.72386 20.5945C5.97391 20.8541 6.31304 21 6.66667 21H17.3333C17.687 21 18.0261 20.8541 18.2761 20.5945C18.5262 20.3348 18.6667 19.9826 18.6667 19.6154V7.15385H19.3333C19.5101 7.15385 19.6797 7.08091 19.8047 6.95107C19.9298 6.82124 20 6.64515 20 6.46154C20 6.27793 19.9298 6.10184 19.8047 5.972C19.6797 5.84217 19.5101 5.76923 19.3333 5.76923ZM17.3333 19.6154H6.66667V7.15385H17.3333V19.6154ZM8 3.69231C8 3.5087 8.07024 3.33261 8.19526 3.20277C8.32029 3.07294 8.48986 3 8.66667 3H15.3333C15.5101 3 15.6797 3.07294 15.8047 3.20277C15.9298 3.33261 16 3.5087 16 3.69231C16 3.87592 15.9298 4.05201 15.8047 4.18184C15.6797 4.31168 15.5101 4.38462 15.3333 4.38462H8.66667C8.48986 4.38462 8.32029 4.31168 8.19526 4.18184C8.07024 4.05201 8 3.87592 8 3.69231Z" fill="#DD5F5F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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

View file

@ -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

View file

@ -2,9 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.belledonne-communications.linphone</string>
<string>group.org.linphone.phone.linphoneExtension</string>
<string>group.org.linphone.phone.msgNotification</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View file

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

View file

@ -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 à labri 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" : {

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
<section name="proxy_default_values">
<entry name="avpf" overwrite="true">1</entry>
<entry name="dial_escape_plus" overwrite="true">0</entry>
<entry name="publish" overwrite="true">1</entry>
<entry name="publish_expires" overwrite="true">120</entry>
<entry name="quality_reporting_collector" overwrite="true">sip:voip-metrics@sip.linphone.org;transport=tls</entry>
<entry name="quality_reporting_enabled" overwrite="true">1</entry>
<entry name="quality_reporting_interval" overwrite="true">180</entry>
<entry name="reg_expires" overwrite="true">31536000</entry>
<entry name="reg_identity" overwrite="true">sip:?@sip.linphone.org</entry>
<entry name="reg_proxy" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
<entry name="reg_route" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
<entry name="reg_sendregister" overwrite="true">1</entry>
<entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</entry>
<entry name="realm" overwrite="true">sip.linphone.org</entry>
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
<entry name="audio_video_conference_factory_uri" overwrite="true">sip:videoconference-factory@sip.linphone.org</entry>
<entry name="push_notification_allowed" overwrite="true">1</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
<entry name="rtp_bundle" overwrite="true">1</entry>
<entry name="lime_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
<entry name="protocols" overwrite="true">stun,ice</entry>
</section>
<section name="sip">
<entry name="media_encryption" overwrite="true">zrtp</entry>
<entry name="media_encryption_mandatory" overwrite="true">1</entry>
</section>
<section name="net">
<entry name="friendlist_subscription_enabled" overwrite="true">1</entry>
</section>
</config>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
<section name="proxy_default_values">
<entry name="avpf" overwrite="true">0</entry>
<entry name="dial_escape_plus" overwrite="true">0</entry>
<entry name="publish" overwrite="true">0</entry>
<entry name="publish_expires" overwrite="true">-1</entry>
<entry name="quality_reporting_collector" overwrite="true"></entry>
<entry name="quality_reporting_enabled" overwrite="true">0</entry>
<entry name="quality_reporting_interval" overwrite="true">0</entry>
<entry name="reg_expires" overwrite="true">3600</entry>
<entry name="reg_identity" overwrite="true"></entry>
<entry name="reg_proxy" overwrite="true"></entry>
<entry name="reg_route" overwrite="true"></entry>
<entry name="reg_sendregister" overwrite="true">1</entry>
<entry name="nat_policy_ref" overwrite="true"></entry>
<entry name="realm" overwrite="true"></entry>
<entry name="conference_factory_uri" overwrite="true"></entry>
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
<entry name="push_notification_allowed" overwrite="true">0</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
<entry name="rtp_bundle" overwrite="true">0</entry>
<entry name="lime_server_url" overwrite="true"></entry>
</section>
</config>

View file

@ -0,0 +1,39 @@
## Start of default rc
[sip]
contact="Linphone iPhone" <sip:linphone.iphone@unknown-host>
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

View file

@ -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

View file

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

View file

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

View file

@ -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 dutilisation](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())
}

View file

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

View file

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

View file

@ -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()
}
}
*/
}
}

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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..<magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count, id: \.self) { index in
if contactViewModel.indexDisplayedFriend != nil && contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
ForEach(0..<contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count, id: \.self) { index in
Button {
} label: {
HStack {
@ -56,7 +69,7 @@ struct ContactInnerActionsFragment: View {
Text("SIP address :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly().dropFirst(4))
Text(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly().dropFirst(4))
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
@ -81,7 +94,7 @@ struct ContactInnerActionsFragment: View {
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy = magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
contactViewModel.stringToCopy = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
showingSheet.toggle()
}
)
@ -94,8 +107,8 @@ struct ContactInnerActionsFragment: View {
}
)
if !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.isEmpty
|| index < magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count - 1 {
if !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.isEmpty
|| index < contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count - 1 {
VStack {
Divider()
}
@ -103,14 +116,14 @@ struct ContactInnerActionsFragment: View {
}
}
ForEach(0..<magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count, id: \.self) { index in
ForEach(0..<contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count, id: \.self) { index in
Button {
} label: {
HStack {
VStack {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label!.isEmpty {
Text("Phone (\(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label!)) :")
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label!.isEmpty {
Text("Phone (\(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label!)) :")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
@ -118,7 +131,7 @@ struct ContactInnerActionsFragment: View {
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
}
Text(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber)
Text(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
@ -144,7 +157,7 @@ struct ContactInnerActionsFragment: View {
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy =
magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
showingSheet.toggle()
}
)
@ -157,7 +170,7 @@ struct ContactInnerActionsFragment: View {
}
)
if index < magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count - 1 {
if index < contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count - 1 {
VStack {
Divider()
}
@ -174,28 +187,28 @@ struct ContactInnerActionsFragment: View {
}
if contactViewModel.indexDisplayedFriend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& ((magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty)
|| (magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!.isEmpty)) {
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& ((contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty)
|| (contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!.isEmpty)) {
VStack {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty {
Text("**Company :** \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!)")
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty {
Text("**Company :** \(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!)")
.default_text_style(styleSize: 14)
.padding(.vertical, 15)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!.isEmpty {
Text("**Job :** \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!)")
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!.isEmpty {
Text("**Job :** \(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.jobTitle!)")
.default_text_style(styleSize: 14)
.padding(.top,
magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.organization!.isEmpty
? 0 : 15
)
.padding(.bottom, 15)
@ -226,10 +239,10 @@ struct ContactInnerActionsFragment: View {
.background(Color.gray100)
VStack(spacing: 0) {
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < magicSearch.lastSearch.count
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < contactsManager.lastSearch.count
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
Button {
actionEditButton()
} label: {
@ -253,6 +266,7 @@ struct ContactInnerActionsFragment: View {
} else {
NavigationLink(destination: EditContactFragment(
editContactViewModel: editContactViewModel,
contactViewModel: contactViewModel,
isShowEditContactFragment: .constant(false),
isShowDismissPopup: $isShowDismissPopup)) {
HStack {
@ -274,7 +288,7 @@ struct ContactInnerActionsFragment: View {
}
.simultaneousGesture(
TapGesture().onEnded {
editContactViewModel.selectedEditFriend = magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend
editContactViewModel.selectedEditFriend = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend
editContactViewModel.resetValues()
}
)
@ -286,22 +300,22 @@ struct ContactInnerActionsFragment: View {
.padding(.horizontal)
Button {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
contactViewModel.objectWillChange.send()
magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred.toggle()
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred.toggle()
}
} label: {
HStack {
Image(contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? "heart-fill" : "heart")
Image(contactViewModel.indexDisplayedFriend != nil && contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? Color.redDanger500 : Color.grayMain2c500)
.foregroundStyle(contactViewModel.indexDisplayedFriend != nil && contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true ? Color.redDanger500 : Color.grayMain2c500)
.frame(width: 25, height: 25)
Text(contactViewModel.indexDisplayedFriend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred == true
? "Remove from favourites"
: "Add to favourites")
.default_text_style(styleSize: 14)
@ -398,7 +412,7 @@ struct ContactInnerActionsFragment: View {
*/
Button {
if magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
isShowDeletePopup.toggle()
}
} label: {

View file

@ -23,10 +23,10 @@ import ContactsUI
struct ContactInnerFragment: View {
@ObservedObject private var sharedMainViewModel = SharedMainViewModel()
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var contactAvatarModel: ContactAvatarModel
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@ -65,10 +65,10 @@ struct ContactInnerFragment: View {
}
Spacer()
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < magicSearch.lastSearch.count
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < contactsManager.lastSearch.count
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!.isEmpty {
Button(action: {
editNativeContact()
}, label: {
@ -82,6 +82,7 @@ struct ContactInnerFragment: View {
} else {
NavigationLink(destination: EditContactFragment(
editContactViewModel: editContactViewModel,
contactViewModel: contactViewModel,
isShowEditContactFragment: .constant(false),
isShowDismissPopup: $isShowDismissPopup)) {
Image("pencil-simple")
@ -93,7 +94,7 @@ struct ContactInnerFragment: View {
}
.simultaneousGesture(
TapGesture().onEnded {
editContactViewModel.selectedEditFriend = magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend
editContactViewModel.selectedEditFriend = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend
editContactViewModel.resetValues()
}
)
@ -109,50 +110,31 @@ struct ContactInnerFragment: View {
VStack(spacing: 0) {
VStack(spacing: 0) {
VStack(spacing: 0) {
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < magicSearch.lastSearch.count
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo != nil
&& !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo!.isEmpty {
AsyncImage(
url: ContactsManager.shared.getImagePath(
friendPhotoPath: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 100, height: 100)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else if contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
if contactViewModel.indexDisplayedFriend != nil && contactViewModel.indexDisplayedFriend! < contactsManager.lastSearch.count
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo != nil
&& !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo!.isEmpty {
Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100)
} else if contactViewModel.indexDisplayedFriend != nil && contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
if contactViewModel.indexDisplayedFriend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend?.name != nil {
Text((magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend?.name)!)
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil
&& contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend?.name != nil {
Text((contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend?.name)!)
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.padding(.top, 10)
Text("En ligne")
.foregroundStyle(Color.greenSuccess500)
Text(contactAvatarModel.lastPresenceInfo)
.foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online"
? Color.greenSuccess500
: Color.orangeWarning600)
.multilineTextAlignment(.center)
.default_text_style_300(styleSize: 12)
.frame(maxWidth: .infinity)
@ -289,7 +271,7 @@ struct ContactInnerFragment: View {
let store = CNContactStore()
let descriptor = CNContactViewController.descriptorForRequiredKeys()
cnContact = try store.unifiedContact(
withIdentifier: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!,
withIdentifier: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.nativeUri!,
keysToFetch: [descriptor]
)
@ -304,6 +286,7 @@ struct ContactInnerFragment: View {
#Preview {
ContactInnerFragment(
contactAvatarModel: ContactAvatarModel(friend: nil, withPresence: true),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
isShowDeletePopup: .constant(false),

View file

@ -24,7 +24,7 @@ struct ContactListBottomSheet: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactViewModel: ContactViewModel
@ -68,6 +68,10 @@ struct ContactListBottomSheet: View {
showingSheet.toggle()
dismiss()
}
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
ToastViewModel.shared.displayToast.toggle()
} label: {
HStack {
Image("copy")

View file

@ -42,10 +42,10 @@ struct ContactsFragment: View {
showingSheet: $showingSheet,
showShareSheet: $showShareSheet
)
.presentationDetents([.fraction(0.2)])
.presentationDetents([.fraction(0.2)])
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!)
ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!)
.presentationDetents([.medium])
.edgesIgnoringSafeArea(.bottom)
}

View file

@ -22,9 +22,8 @@ import linphonesw
struct ContactsInnerFragment: View {
@Environment(\.scenePhase) var scenePhase
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@State private var isFavoriteOpen = true
@ -33,7 +32,7 @@ struct ContactsInnerFragment: View {
var body: some View {
VStack(alignment: .leading) {
if !magicSearch.lastSearch.filter({ $0.friend?.starred == true }).isEmpty {
if !contactsManager.lastSearch.filter({ $0.friend?.starred == true }).isEmpty {
HStack(alignment: .center) {
Text("Favourites")
.default_text_style_800(styleSize: 16)
@ -73,19 +72,31 @@ struct ContactsInnerFragment: View {
.padding(.top, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)
}
.navigationBarHidden(true)
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
ContactsManager.shared.fetchContacts()
print("Active")
} else if newPhase == .inactive {
print("Inactive")
} else if newPhase == .background {
print("Background")
VStack {
List {
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)}
.listStyle(.plain)
.overlay(
VStack {
if contactsManager.lastSearch.isEmpty {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text("No contacts for the moment...")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
}
}
.padding(.all)
)
}
}
.navigationBarHidden(true)
}
}

View file

@ -27,7 +27,6 @@ struct ContactsListBottomSheet: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@State private var orientation = UIDevice.current.orientation
@ -61,7 +60,8 @@ struct ContactsListBottomSheet: View {
if contactViewModel.selectedFriend != nil {
contactViewModel.selectedFriend!.starred.toggle()
}
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
if #available(iOS 16.0, *) {
if idiom != .pad {

View file

@ -21,122 +21,83 @@ import SwiftUI
import linphonesw
struct ContactsListFragment: View {
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var contactsListViewModel: ContactsListViewModel
@Binding var showingSheet: Bool
var body: some View {
VStack {
List {
ForEach(0..<magicSearch.lastSearch.count, id: \.self) { index in
Button {
} label: {
HStack {
if index == 0
|| magicSearch.lastSearch[index].friend?.name!.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first
!= magicSearch.lastSearch[index-1].friend?.name!.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first {
Text(
String(
(magicSearch.lastSearch[index].friend?.name!.uppercased().folding(
options: .diacriticInsensitive,
locale: .current
).first)!))
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, -5)
.padding(.trailing, 10)
} else {
Text("")
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, -5)
.padding(.trailing, 10)
}
if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 45, height: 45)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
Text((magicSearch.lastSearch[index].friend?.name)!)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = magicSearch.lastSearch[index].friend
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
}
)
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
.overlay(
VStack {
if magicSearch.lastSearch.isEmpty {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text("No contacts for the moment...")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
}
}
.padding(.all)
)
}
}
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var contactsListViewModel: ContactsListViewModel
@Binding var showingSheet: Bool
var body: some View {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
Button {
} label: {
HStack {
if index == 0
|| contactsManager.lastSearch[index].friend?.name!.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first
!= contactsManager.lastSearch[index-1].friend?.name!.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first {
Text(
String(
(contactsManager.lastSearch[index].friend?.name!.uppercased().folding(
options: .diacriticInsensitive,
locale: .current
).first)!))
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, -5)
.padding(.trailing, 10)
} else {
Text("")
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, -5)
.padding(.trailing, 10)
}
if index < contactsManager.avatarListModel.count
&& contactsManager.avatarListModel[index].friend!.photo != nil
&& !contactsManager.avatarListModel[index].friend!.photo!.isEmpty {
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 45)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
Text((contactsManager.lastSearch[index].friend?.name)!)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
}
)
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
}
#Preview {
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false))
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false))
}

View file

@ -23,10 +23,12 @@ import linphonesw
struct EditContactFragment: View {
@ObservedObject var editContactViewModel: EditContactViewModel
@ObservedObject private var sharedMainViewModel = SharedMainViewModel()
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@Environment(\.dismiss) var dismiss
var contactViewModel: ContactViewModel
@Binding var isShowEditContactFragment: Bool
@Binding var isShowDismissPopup: Bool
@ -129,26 +131,9 @@ struct EditContactFragment: View {
if editContactViewModel.selectedEditFriend != nil
&& editContactViewModel.selectedEditFriend!.photo != nil
&& !editContactViewModel.selectedEditFriend!.photo!.isEmpty && selectedImage == nil && !removedImage {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: editContactViewModel.selectedEditFriend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 100, height: 100)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
Avatar(contactAvatarModel: ContactAvatarModel(friend: editContactViewModel.selectedEditFriend!, withPresence: false), avatarSize: 100)
} else if selectedImage == nil {
Image("profil-picture-default")
.resizable()
@ -494,7 +479,7 @@ struct EditContactFragment: View {
)
if editContactViewModel.selectedEditFriend != nil && selectedImage == nil &&
!removedImage {
!removedImage && editContactViewModel.selectedEditFriend!.photo!.suffix(11) != "default.png" {
ContactsManager.shared.saveFriend(
result: String(editContactViewModel.selectedEditFriend!.photo!.dropFirst(6)),
contact: newContact,
@ -505,15 +490,23 @@ struct EditContactFragment: View {
image: selectedImage
?? ContactsManager.shared.textToImage(
firstName: editContactViewModel.firstName, lastName: editContactViewModel.lastName),
name: editContactViewModel.firstName
+ editContactViewModel.lastName
+ String(Int.random(in: 1...1000))
+ ((selectedImage == nil) ? "-default" : ""),
name: editContactViewModel.firstName
+ editContactViewModel.lastName,
prefix: ((selectedImage == nil) ? "-default" : ""),
contact: newContact, linphoneFriend: true, existingFriend: editContactViewModel.selectedEditFriend)
}
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
if editContactViewModel.selectedEditFriend != nil && editContactViewModel.selectedEditFriend!.name != editContactViewModel.firstName + " " + editContactViewModel.lastName {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let result = ContactsManager.shared.lastSearch.firstIndex(where: {
$0.friend!.name == newContact.firstName + " " + newContact.lastName
})
contactViewModel.indexDisplayedFriend = result
}
}
delayColorDismiss()
if editContactViewModel.selectedEditFriend == nil {
withAnimation {
@ -527,5 +520,10 @@ struct EditContactFragment: View {
}
#Preview {
EditContactFragment(editContactViewModel: EditContactViewModel(), isShowEditContactFragment: .constant(false), isShowDismissPopup: .constant(false))
EditContactFragment(
editContactViewModel: EditContactViewModel(),
contactViewModel: ContactViewModel(),
isShowEditContactFragment: .constant(false),
isShowDismissPopup: .constant(false)
)
}

View file

@ -21,9 +21,9 @@ import SwiftUI
import linphonesw
struct FavoriteContactsListFragment: View {
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var favoriteContactsListViewModel: FavoriteContactsListViewModel
@ -32,41 +32,21 @@ struct FavoriteContactsListFragment: View {
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(0..<magicSearch.lastSearch.count, id: \.self) { index in
if magicSearch.lastSearch[index].friend != nil && magicSearch.lastSearch[index].friend!.starred == true {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
if contactsManager.lastSearch[index].friend != nil && contactsManager.lastSearch[index].friend!.starred == true {
Button {
} label: {
VStack {
if magicSearch.lastSearch[index].friend!.photo != nil
&& !magicSearch.lastSearch[index].friend!.photo!.isEmpty {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.photo!)
) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 45, height: 45)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
if contactsManager.lastSearch[index].friend!.photo != nil
&& !contactsManager.lastSearch[index].friend!.photo!.isEmpty {
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 45)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
Text((magicSearch.lastSearch[index].friend?.name)!)
Text((contactsManager.lastSearch[index].friend?.name)!)
.default_text_style(styleSize: 16)
.frame( maxWidth: .infinity, alignment: .center)
}
@ -74,7 +54,7 @@ struct FavoriteContactsListFragment: View {
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = magicSearch.lastSearch[index].friend
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
showingSheet.toggle()
}
)

View file

@ -0,0 +1,121 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
}
}

View file

@ -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() {}
}

View file

@ -20,6 +20,6 @@
import linphonesw
class ContactsListViewModel: ObservableObject {
init() {}
}

View file

@ -17,18 +17,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// 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("Dont 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

View file

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

View file

@ -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"),

View file

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

View file

@ -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<String>) -> some View {
self.modifier(ToastView(sharedMainViewModel: SharedMainViewModel(), isShowing: isShowing))
.transition(.move(edge: .top))
.padding(.top, 60)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
)
}

View file

@ -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..<callLogsFilter.count, id: \.self) { index in
HStack {
VStack {
Image(historyListViewModel.getCallIconResId(callStatus: callLogsFilter[index].status, callDir: callLogsFilter[index].dir))
.resizable()
.frame(
width: historyListViewModel.getCallIconResId(
callStatus: callLogsFilter[index].status,
callDir: callLogsFilter[index].dir
).contains("rejected") ? 12 : 8,
height: historyListViewModel.getCallIconResId(
callStatus: callLogsFilter[index].status,
callDir: callLogsFilter[index].dir
).contains("rejected") ? 6 : 8)
.padding(.top, 5)
Spacer()
}
VStack {
Text(historyListViewModel.getCallText(
callStatus: callLogsFilter[index].status,
callDir: callLogsFilter[index].dir)
)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(historyListViewModel.getCallTime(startDate: callLogsFilter[index].startDate))
.foregroundStyle(
callLogsFilter[index].status != .Success
? Color.redDanger500
: Color.grayMain2c600
)
.default_text_style_300(styleSize: 12)
.frame(maxWidth: .infinity, alignment: .leading)
}
VStack {
Spacer()
Text(callLogsFilter[index].duration.convertDurationToString())
.default_text_style_300(styleSize: 12)
Spacer()
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
}
.background(.white)
.cornerRadius(15)
.padding(.all)
}
.frame(maxWidth: sharedMainViewModel.maxWidth)
}
.frame(maxWidth: .infinity)
}
.background(Color.gray100)
}
.background(.white)
.navigationBarHidden(true)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
.navigationViewStyle(.stack)
}
}
#Preview {
HistoryContactFragment()
HistoryContactFragment(
contactAvatarModel: ContactAvatarModel(friend: nil, withPresence: false),
historyViewModel: HistoryViewModel(),
historyListViewModel: HistoryListViewModel(),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
isShowDeleteAllHistoryPopup: .constant(false),
isShowEditContactFragment: .constant(false),
indexPage: .constant(1)
)
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
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)
)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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..<historyListViewModel.callLogs.count, id: \.self) { index in
Button {
} label: {
HStack {
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
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)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45)
}
} else {
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
if historyListViewModel.callLogs[index].toAddress!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: historyListViewModel.callLogs[index].toAddress!.displayName!,
lastName: historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ").count > 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))
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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..<contactsManager.lastSearchSuggestions.count, id: \.self) { index in
Button {
} label: {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil
&& contactsManager.lastSearchSuggestions[index].address!.username != nil {
Image(uiImage: contactsManager.textToImage(
firstName: contactsManager.lastSearchSuggestions[index].address!.username!,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(contactsManager.lastSearchSuggestions[index].address?.username ?? "")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text("Username error")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
.padding(.horizontal)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
}
)
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
}
#Preview {
StartCallFragment(startCallViewModel: StartCallViewModel(), isShowStartCallFragment: .constant(true), showingDialer: .constant(false))
}

View file

@ -18,25 +18,45 @@
*/
import SwiftUI
import linphonesw
struct HistoryView: View {
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@Binding var index: Int
@Binding var isShowStartCallFragment: Bool
@Binding var isShowEditContactFragment: Bool
var body: some View {
NavigationView {
VStack(spacing: 0) {
VStack {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text("No calls for the moment...")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
ZStack(alignment: .bottomTrailing) {
HistoryFragment(
historyListViewModel: historyListViewModel,
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
Button {
withAnimation {
MagicSearchSingleton.shared.searchForSuggestions()
isShowStartCallFragment.toggle()
}
} label: {
Image("phone-plus")
.padding()
.background(.white)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 4)
}
.padding(.all)
.padding()
}
}
.navigationViewStyle(.stack)
@ -44,5 +64,12 @@ struct HistoryView: View {
}
#Preview {
HistoryView()
HistoryFragment(
historyListViewModel: HistoryListViewModel(),
historyViewModel: HistoryViewModel(),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
index: .constant(1),
isShowEditContactFragment: .constant(false)
)
}

View file

@ -0,0 +1,222 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import linphonesw
class 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
}
}
}
}

View file

@ -18,10 +18,13 @@
*/
import Foundation
import linphonesw
class HistoryViewModel: ObservableObject {
@Published var historyTitle: String = ""
@Published var displayedCall: CallLog?
var selectedCall: CallLog?
init() {}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
import linphonesw
class StartCallViewModel: ObservableObject {
@Published var searchField: String = ""
init() {}
}

View file

@ -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 {

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
import Foundation
class ToastViewModel: ObservableObject {
static let shared = ToastViewModel()
var toastMessage: String = ""
@Published var displayToast = false
private init() {
}
}

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}
}
}

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)"
}
}

View file

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

View file

@ -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 {