From e47a04c5d9e0065bfdc7d3823d60dffcb2b02c01 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 1 Dec 2023 16:36:11 +0100 Subject: [PATCH] Start new call view --- Linphone.xcodeproj/project.pbxproj | 12 + .../dialer.imageset/Contents.json | 21 ++ .../dialer.imageset/dialer.svg | 3 + Linphone/Contacts/ContactsManager.swift | 13 +- Linphone/Core/CoreContext.swift | 5 +- Linphone/LinphoneApp.swift | 12 +- Linphone/Localizable.xcstrings | 51 +++ .../Fragments/PermissionsFragment.swift | 5 +- .../Fragments/ContactsInnerFragment.swift | 24 +- .../Fragments/ContactsListFragment.swift | 174 +++++----- .../Fragments/EditContactFragment.swift | 4 +- Linphone/UI/Main/ContentView.swift | 32 +- .../History/Fragments/DialerBottomSheet.swift | 320 ++++++++++++++++++ .../Fragments/HistoryListFragment.swift | 4 +- .../History/Fragments/StartCallFragment.swift | 245 ++++++++++++++ Linphone/UI/Main/History/HistoryView.swift | 6 + .../ViewModel/StartCallViewModel.swift | 27 ++ Linphone/Utils/EditContactController.swift | 3 +- Linphone/Utils/MagicSearchSingleton.swift | 35 +- Linphone/Utils/PermissionManager.swift | 7 + 20 files changed, 876 insertions(+), 127 deletions(-) create mode 100644 Linphone/Assets.xcassets/dialer.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/dialer.imageset/dialer.svg create mode 100644 Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift create mode 100644 Linphone/UI/Main/History/Fragments/StartCallFragment.swift create mode 100644 Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 60a8812a8..3636ae410 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 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 */; }; @@ -51,6 +53,7 @@ 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 */; }; @@ -106,6 +109,8 @@ D72343332ACEFFC3009AA24E /* QRScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanner.swift; sourceTree = ""; }; D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D726E4382B16440C0083C415 /* ContactAvatarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAvatarModel.swift; sourceTree = ""; }; + D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallFragment.swift; sourceTree = ""; }; + D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.swift; sourceTree = ""; }; D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = ""; }; D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = ""; }; D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = ""; }; @@ -129,6 +134,7 @@ D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = ""; }; D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = ""; }; + D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = ""; }; D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; @@ -295,6 +301,7 @@ children = ( D72250622ADE9615008FB426 /* HistoryViewModel.swift */, D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */, + D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -314,6 +321,8 @@ D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */, D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */, D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */, + D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */, + D79622332B1DFE600037EACD /* DialerBottomSheet.swift */, ); path = Fragments; sourceTree = ""; @@ -576,6 +585,7 @@ 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 */, @@ -605,7 +615,9 @@ D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */, D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */, D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, + D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */, D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */, + D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */, diff --git a/Linphone/Assets.xcassets/dialer.imageset/Contents.json b/Linphone/Assets.xcassets/dialer.imageset/Contents.json new file mode 100644 index 000000000..117f088cd --- /dev/null +++ b/Linphone/Assets.xcassets/dialer.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dialer.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/dialer.imageset/dialer.svg b/Linphone/Assets.xcassets/dialer.imageset/dialer.svg new file mode 100644 index 000000000..71705dfdf --- /dev/null +++ b/Linphone/Assets.xcassets/dialer.imageset/dialer.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 6b80d8131..139e62338 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -35,6 +35,7 @@ final class ContactsManager: ObservableObject { var linphoneFriendList: FriendList? @Published var lastSearch: [SearchResult] = [] + @Published var lastSearchSuggestions: [SearchResult] = [] @Published var avatarListModel: [ContactAvatarModel] = [] private init() { @@ -121,7 +122,8 @@ final class ContactsManager: ObservableObject { && 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: String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: nil) } }) @@ -167,12 +169,12 @@ final class ContactsManager: ObservableObject { 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 { @@ -260,15 +262,16 @@ final class ContactsManager: ObservableObject { 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 urlName = URL(string: name + prefix) let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : String(Int.random(in: 1...1000)) let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png")) + completion(decodedData, imagePath + ".png") } catch { print("Error: ", error) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index b6699bb8c..ce88facec 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -87,15 +87,18 @@ final class CoreContext: ObservableObject { self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" + print("configDirconfigDirconfigDir \(configDir)") + 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 } - self.coreIsStarted = true } try? self.mCore.start() diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 159abb97d..61e22bdf3 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -29,6 +29,7 @@ struct LinphoneApp: App { @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 { @@ -37,17 +38,21 @@ struct LinphoneApp: App { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { AssistantView() - } else if coreContext.defaultAccount != nil + } else if coreContext.defaultAccount != nil && contactViewModel != nil && editContactViewModel != nil && historyViewModel != nil - && historyListViewModel != nil { + && historyListViewModel != nil + && startCallViewModel != nil { ContentView( contactViewModel: contactViewModel!, editContactViewModel: editContactViewModel!, historyViewModel: historyViewModel!, - historyListViewModel: historyListViewModel! + historyListViewModel: historyListViewModel!, + startCallViewModel: startCallViewModel! ) + } else { + SplashScreen() } } else { SplashScreen() @@ -56,6 +61,7 @@ struct LinphoneApp: App { editContactViewModel = EditContactViewModel() historyViewModel = HistoryViewModel() historyListViewModel = HistoryListViewModel() + startCallViewModel = StartCallViewModel() } } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 394b1e7f0..569f38f71 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -27,6 +27,9 @@ }, "[notre politique de confidentialité](https://linphone.org/privacy-policy)" : { + }, + "*" : { + }, "**Camera** : Pour capturer votre vidéo lors des appels vidéo et conférence." : { @@ -45,6 +48,9 @@ }, "**Notifications** : Pour vous informé quand vous recevez un message ou un appel." : { + }, + "#" : { + }, "%lld Book (Example)" : { "extractionState" : "manual", @@ -98,6 +104,39 @@ } } } + }, + "+" : { + + }, + "0" : { + + }, + "1" : { + + }, + "2" : { + + }, + "3" : { + + }, + "4" : { + + }, + "5" : { + + }, + "6" : { + + }, + "7" : { + + }, + "8" : { + + }, + "9" : { + }, "Accept all" : { @@ -313,6 +352,9 @@ }, "My Profile" : { + }, + "New call" : { + }, "New contact" : { @@ -393,6 +435,9 @@ }, "Scan QR code" : { + }, + "Search contact or history call" : { + }, "Sécurisé" : { @@ -429,6 +474,9 @@ }, "Start" : { + }, + "Suggestions" : { + }, "TCP" : { @@ -479,6 +527,9 @@ } } } + }, + "Username error" : { + }, "Video Call" : { diff --git a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift index 82f709bac..fb7eed70a 100644 --- a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift +++ b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift @@ -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() diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift index d1cb1215f..0168bb88f 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift @@ -72,7 +72,29 @@ struct ContactsInnerFragment: View { .padding(.top, 10) .padding(.horizontal, 16) } - ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet) + + 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) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 0857c53aa..f48664a24 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -21,105 +21,83 @@ import SwiftUI import linphonesw struct ContactsListFragment: View { - - @ObservedObject var contactsManager = ContactsManager.shared - - @ObservedObject var contactViewModel: ContactViewModel - @ObservedObject var contactsListViewModel: ContactsListViewModel - - @Binding var showingSheet: Bool - - var body: some View { - VStack { - List { - ForEach(0... + */ + +import SwiftUI +import UniformTypeIdentifiers + +struct DialerBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject private var magicSearch = MagicSearchSingleton.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var startCallViewModel: StartCallViewModel + + @State private var orientation = UIDevice.current.orientation + + @Binding var showingDialer: Bool + + var body: some View { + VStack(alignment: .center, spacing: 0) { + VStack(alignment: .center, spacing: 0) { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("Close") { + showingDialer.toggle() + dismiss() + } + } + .padding(.trailing) + } else { + Capsule() + .fill(Color.grayMain2c300) + .frame(width: 75, height: 5) + .padding(15) + } + + Spacer() + + HStack { + Button { + startCallViewModel.searchField += "1" + } label: { + Text("1") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "2" + } label: { + Text("2") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "3" + } label: { + Text("3") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "4" + } label: { + Text("4") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "5" + } label: { + Text("5") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "6" + } label: { + Text("6") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "7" + } label: { + Text("7") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "8" + } label: { + Text("8") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "9" + } label: { + Text("9") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "*" + } label: { + Text("*") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + } label: { + ZStack { + Text("0") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 75) + .padding(.top, -15) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + Text("+") + .default_text_style(styleSize: 20) + .multilineTextAlignment(.center) + .frame(width: 60, height: 85) + .padding(.bottom, -25) + .background(.clear) + .clipShape(Circle()) + } + } + .simultaneousGesture( + LongPressGesture() + .onEnded { _ in + startCallViewModel.searchField += "+" + } + ) + .highPriorityGesture( + TapGesture() + .onEnded { _ in + startCallViewModel.searchField += "0" + } + ) + + Spacer() + + Button { + startCallViewModel.searchField += "#" + } label: { + Text("#") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + + HStack { + + } + .frame(width: 60, height: 60) + + Spacer() + + Button { + } label: { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 90, height: 60) + .background(Color.greenSuccess500) + .cornerRadius(40) + .shadow(color: .black.opacity(0.2), radius: 4) + + Spacer() + + Button { + startCallViewModel.searchField = String(startCallViewModel.searchField.dropLast()) + } label: { + Image("backspace-fill") + .resizable() + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + } + .padding(.horizontal, 60) + .padding(.top, 20) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + .onRotate { newOrientation in + orientation = newOrientation + } + } +} + +#Preview { + DialerBottomSheet( + startCallViewModel: StartCallViewModel(), showingDialer: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index 50fd2513a..a2fcac515 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -49,7 +49,9 @@ struct HistoryListFragment: View { : ContactAvatarModel(friend: nil, withPresence: false) if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { - Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45) + 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 { diff --git a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift new file mode 100644 index 000000000..b03dde24e --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI +import linphonesw + +struct StartCallFragment: View { + + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var startCallViewModel: StartCallViewModel + + @Binding var isShowStartCallFragment: Bool + @Binding var showingDialer: Bool + + @FocusState var isSearchFieldFocused: Bool + @State private var hasTimeElapsed = false + @State private var delayedColor = Color.white + + var body: some View { + ZStack { + VStack(spacing: 1) { + + Rectangle() + .foregroundColor(delayedColor) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + .task(delayColor) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) + .onTapGesture { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + + startCallViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + delayColorDismiss() + withAnimation { + isShowStartCallFragment.toggle() + } + } + + Text("New call") + .multilineTextAlignment(.leading) + .default_text_style_orange_800(styleSize: 16) + + Spacer() + + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + VStack(spacing: 0) { + ZStack(alignment: .trailing) { + TextField("Search contact or history call", text: $startCallViewModel.searchField) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isSearchFieldFocused) + .padding(.horizontal, 30) + .onChange(of: startCallViewModel.searchField) { newValue in + magicSearch.currentFilterSuggestions = newValue + magicSearch.searchForSuggestions() + } + + HStack { + Button(action: { + }, label: { + Image("magnifying-glass") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + + Spacer() + + if startCallViewModel.searchField.isEmpty { + Button(action: { + isSearchFieldFocused = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + showingDialer.toggle() + } + }, label: { + Image(!showingDialer ? "dialer" : "keyboard") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + } else { + Button(action: { + startCallViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + magicSearch.searchForSuggestions() + }, label: { + Image("x") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + } + } + } + .padding(.horizontal, 15) + .padding(.vertical, 10) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSearchFieldFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.vertical) + .padding(.horizontal) + + ScrollView { + if !ContactsManager.shared.lastSearch.isEmpty { + HStack(alignment: .center) { + Text("All contacts") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + } + + ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false)) + .padding(.horizontal, 16) + + HStack(alignment: .center) { + Text("Suggestions") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } + } + .frame(maxWidth: .infinity) + } + .background(.white) + } + .navigationBarHidden(true) + } + + @Sendable private func delayColor() async { + try? await Task.sleep(nanoseconds: 250_000_000) + delayedColor = Color.orangeMain500 + } + + func delayColorDismiss() { + Task { + try? await Task.sleep(nanoseconds: 80_000_000) + delayedColor = .white + } + } + + var suggestionsList: some View { + ForEach(0... + */ + +import linphonesw + +class StartCallViewModel: ObservableObject { + + @Published var searchField: String = "" + + init() {} +} diff --git a/Linphone/Utils/EditContactController.swift b/Linphone/Utils/EditContactController.swift index b29718a4e..df8e0c67f 100644 --- a/Linphone/Utils/EditContactController.swift +++ b/Linphone/Utils/EditContactController.swift @@ -50,7 +50,8 @@ 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: String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact)) diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 488eb5d4b..0cd349bb8 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -30,6 +30,9 @@ final class MagicSearchSingleton: ObservableObject { var currentFilter: String = "" var previousFilter: String? + var currentFilterSuggestions: String = "" + var previousFilterSuggestions: String? + var needUpdateLastSearchContacts = false private var limitSearchToLinphoneAccounts = true @@ -46,11 +49,27 @@ final class MagicSearchSingleton: ObservableObject { self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in self.needUpdateLastSearchContacts = true - self.contactsManager.lastSearch = magicSearch.lastSearch.sorted(by: { + + 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() @@ -91,26 +110,26 @@ final class MagicSearchSingleton: ObservableObject { } } - func searchForContactsWithResult(sourceFlags: Int) { + func searchForSuggestions() { coreContext.doOnCoreQueue { _ in var needResetCache = false DispatchQueue.main.sync { - if let oldFilter = self.previousFilter { - if oldFilter.count > self.currentFilter.count || oldFilter != self.currentFilter { + if let oldFilter = self.previousFilterSuggestions { + if oldFilter.count > self.currentFilterSuggestions.count || oldFilter != self.currentFilterSuggestions { needResetCache = true } } - self.previousFilter = self.currentFilter + self.previousFilterSuggestions = self.currentFilterSuggestions } if needResetCache { self.magicSearch.resetSearchCache() } self.magicSearch.getContactsListAsync( - filter: self.currentFilter, - domain: self.allContact ? "" : self.domainDefaultAccount, - sourceFlags: sourceFlags, + filter: self.currentFilterSuggestions, + domain: self.domainDefaultAccount, + sourceFlags: MagicSearch.Source.All.rawValue, aggregation: MagicSearch.Aggregation.Friend) } } diff --git a/Linphone/Utils/PermissionManager.swift b/Linphone/Utils/PermissionManager.swift index f1d741f7e..e19833012 100644 --- a/Linphone/Utils/PermissionManager.swift +++ b/Linphone/Utils/PermissionManager.swift @@ -31,6 +31,13 @@ class PermissionManager: ObservableObject { private init() {} + + func getPermissions(){ + photoLibraryRequestPermission() + cameraRequestPermission() + contactsRequestPermission() + } + func photoLibraryRequestPermission() { PHPhotoLibrary.requestAuthorization(for: .readWrite, handler: {status in DispatchQueue.main.async {