From da68a15694017377189b500241e0283432c2a88c Mon Sep 17 00:00:00 2001 From: "benoit.martins" Date: Thu, 11 Jul 2024 17:24:28 +0200 Subject: [PATCH] Create conversation --- Linphone.xcodeproj/project.pbxproj | 32 +- Linphone/LinphoneApp.swift | 4 + Linphone/Localizable.xcstrings | 57 ++- .../Fragments/ContactsListFragment.swift | 1 + Linphone/UI/Main/ContentView.swift | 46 +- .../Conversations/ConversationsView.swift | 9 + .../Fragments/ConversationsListFragment.swift | 24 +- .../Fragments/StartConversationFragment.swift | 397 ++++++++++++++++++ .../StartConversationViewModel.swift | 320 ++++++++++++++ .../History/Fragments/StartCallFragment.swift | 22 +- Linphone/UI/Main/History/HistoryView.swift | 5 +- Linphone/Utils/LinphoneUtils.swift | 5 + 12 files changed, 873 insertions(+), 49 deletions(-) create mode 100644 Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift create mode 100644 Linphone/UI/Main/Conversations/ViewModel/StartConversationViewModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 51b076f89..ce886bd33 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -109,6 +109,8 @@ D74DA0122C047F0700A8561D /* HistoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74DA0112C047F0700A8561D /* HistoryModel.swift */; }; D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */; }; D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75759312B56D40900E7AC10 /* ZRTPPopup.swift */; }; + D759CB642C3FBD4200AC35E8 /* StartConversationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */; }; + D759CB662C3FBE1D00AC35E8 /* StartConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.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 */; }; @@ -288,6 +290,8 @@ D74DA0112C047F0700A8561D /* HistoryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryModel.swift; sourceTree = ""; }; D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = ""; }; D75759312B56D40900E7AC10 /* ZRTPPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZRTPPopup.swift; sourceTree = ""; }; + D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartConversationFragment.swift; sourceTree = ""; }; + D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartConversationViewModel.swift; sourceTree = ""; }; D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; @@ -813,6 +817,7 @@ children = ( D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */, D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */, + D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -826,6 +831,7 @@ D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */, D71968912B86369D00DF4459 /* ChatBubbleView.swift */, D72A9A042B9750A1000DC093 /* UIList.swift */, + D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -1043,6 +1049,7 @@ D78E062A2BEA698E00CE3783 /* MediaEncryptedSheetBottomSheet.swift in Sources */, D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */, D71A0E192B485ADF0002C6CD /* ViewExtension.swift in Sources */, + D759CB642C3FBD4200AC35E8 /* StartConversationFragment.swift in Sources */, D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */, D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */, 662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */, @@ -1054,6 +1061,7 @@ D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */, C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */, D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */, + D759CB662C3FBE1D00AC35E8 /* StartConversationViewModel.swift in Sources */, D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */, D720E6AD2BAD822000DDFD87 /* ParticipantModel.swift in Sources */, D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */, @@ -1193,6 +1201,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", + "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; @@ -1206,7 +1215,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1232,7 +1241,10 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "USE_CRASHLYTICS=1", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = msgNotificationService; @@ -1245,7 +1257,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1382,7 +1394,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 27; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; @@ -1392,6 +1404,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", + "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; @@ -1418,7 +1431,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 6.0.0; - OTHER_SWIFT_FLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1438,13 +1451,16 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 27; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "USE_CRASHLYTICS=1", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; @@ -1470,7 +1486,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 6.0.0; - OTHER_SWIFT_FLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index a720905eb..c67122247 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -75,6 +75,7 @@ struct LinphoneApp: App { @State private var historyViewModel: HistoryViewModel? @State private var historyListViewModel: HistoryListViewModel? @State private var startCallViewModel: StartCallViewModel? + @State private var startConversationViewModel: StartConversationViewModel? @State private var callViewModel: CallViewModel? @State private var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel? @State private var conversationsListViewModel: ConversationsListViewModel? @@ -106,6 +107,7 @@ struct LinphoneApp: App { && historyViewModel != nil && historyListViewModel != nil && startCallViewModel != nil + && startConversationViewModel != nil && callViewModel != nil && meetingWaitingRoomViewModel != nil && conversationsListViewModel != nil @@ -118,6 +120,7 @@ struct LinphoneApp: App { historyViewModel: historyViewModel!, historyListViewModel: historyListViewModel!, startCallViewModel: startCallViewModel!, + startConversationViewModel: startConversationViewModel!, callViewModel: callViewModel!, meetingWaitingRoomViewModel: meetingWaitingRoomViewModel!, conversationsListViewModel: conversationsListViewModel!, @@ -140,6 +143,7 @@ struct LinphoneApp: App { historyViewModel = HistoryViewModel() historyListViewModel = HistoryListViewModel() startCallViewModel = StartCallViewModel() + startConversationViewModel = StartConversationViewModel() callViewModel = CallViewModel() meetingWaitingRoomViewModel = MeetingWaitingRoomViewModel() conversationsListViewModel = ConversationsListViewModel() diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 47e7b0f18..e2bafb067 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -1188,6 +1188,40 @@ } } }, + "history_call_start_search_bar_filter_hint" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Search contact or history call" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cherchez un contact ou une suggestion" + } + } + } + }, + "history_call_start_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New call" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouvel appel" + } + } + } + }, "history_group_call_start_dialog_set_subject" : { "localizations" : { "en" : { @@ -1306,9 +1340,6 @@ }, "Joining..." : { - }, - "Key" : { - "extractionState" : "manual" }, "Last name" : { @@ -1389,6 +1420,23 @@ }, "New contact" : { + }, + "new_conversation_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New conversation" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Créer une conversation" + } + } + } }, "Next" : { @@ -1541,9 +1589,6 @@ }, "Search contact" : { - }, - "Search contact or history call" : { - }, "Sécurisé" : { diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 9c57fb39d..dec374cfc 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -87,6 +87,7 @@ struct ContactsListFragment: View { withAnimation { contactViewModel.indexDisplayedFriend = index } + if index < contactsManager.lastSearch.count && contactsManager.lastSearch[index].friend != nil && contactsManager.lastSearch[index].friend!.address != nil { startCallFunc(contactsManager.lastSearch[index].friend!.address!) } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index f41f07872..307bae4d9 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -39,6 +39,7 @@ struct ContentView: View { @ObservedObject var historyViewModel: HistoryViewModel @ObservedObject var historyListViewModel: HistoryListViewModel @ObservedObject var startCallViewModel: StartCallViewModel + @ObservedObject var startConversationViewModel: StartConversationViewModel @ObservedObject var callViewModel: CallViewModel @ObservedObject var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel @@ -59,6 +60,7 @@ struct ContentView: View { @State var isShowDeleteAllHistoryPopup = false @State var isShowEditContactFragment = false @State var isShowStartCallFragment = false + @State var isShowStartConversationFragment = false @State var isShowDismissPopup = false @State var isShowSendCancelMeetingNotificationPopup = false @State var isShowSipAddressesPopup = false @@ -70,7 +72,7 @@ struct ContentView: View { var body: some View { let pub = NotificationCenter.default - .publisher(for: NSNotification.Name("ContactLoaded")) + .publisher(for: NSNotification.Name("ContactLoaded")) GeometryReader { geometry in VStack(spacing: 0) { @@ -103,9 +105,9 @@ struct ContentView: View { .frame(height: 30) .background(Color.greenSuccess500) .onTapGesture { - withAnimation { - telecomManager.callDisplayed = true - } + withAnimation { + telecomManager.callDisplayed = true + } } } @@ -542,7 +544,12 @@ struct ContentView: View { text: $text ) } else if self.index == 2 { - ConversationsView(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, text: $text) + ConversationsView( + conversationViewModel: conversationViewModel, + conversationsListViewModel: conversationsListViewModel, + text: $text, + isShowStartConversationFragment: $isShowStartConversationFragment + ) } else if self.index == 3 { MeetingsView( meetingsListViewModel: meetingsListViewModel, @@ -778,16 +785,16 @@ struct ContentView: View { } } else if self.index == 2 { ConversationFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel) - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) } else if self.index == 3 { MeetingFragment(meetingViewModel: meetingViewModel, meetingsListViewModel: meetingsListViewModel, isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment, isShowSendCancelMeetingNotificationPopup: $isShowSendCancelMeetingNotificationPopup) - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) } - + } .onAppear { if !(orientation == .landscapeLeft @@ -888,7 +895,7 @@ struct ContentView: View { DialerBottomSheet( startCallViewModel: startCallViewModel, callViewModel: callViewModel, - isShowStartCallFragment: $isShowStartCallFragment, + isShowStartCallFragment: $isShowStartCallFragment, showingDialer: $showingDialer, currentCall: nil ) @@ -896,6 +903,16 @@ struct ContentView: View { } } + if isShowStartConversationFragment { + StartConversationFragment( + startConversationViewModel: startConversationViewModel, + conversationViewModel: conversationViewModel, + isShowStartConversationFragment: $isShowStartConversationFragment + ) + .zIndex(6) + .transition(.opacity.combined(with: .move(edge: .bottom))) + } + if isShowDeleteContactPopup { PopupView(isShowPopup: $isShowDeleteContactPopup, title: Text( @@ -1082,7 +1099,7 @@ struct ContentView: View { .onReceive(pub) { _ in conversationsListViewModel.refreshContactAvatarModel() historyListViewModel.refreshHistoryAvatarModel() - } + } } .overlay { if isMenuOpen { @@ -1118,6 +1135,7 @@ struct ContentView: View { historyViewModel: HistoryViewModel(), historyListViewModel: HistoryListViewModel(), startCallViewModel: StartCallViewModel(), + startConversationViewModel: StartConversationViewModel(), callViewModel: CallViewModel(), meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel(), conversationsListViewModel: ConversationsListViewModel(), diff --git a/Linphone/UI/Main/Conversations/ConversationsView.swift b/Linphone/UI/Main/Conversations/ConversationsView.swift index 0a72dd314..ea219b8a7 100644 --- a/Linphone/UI/Main/Conversations/ConversationsView.swift +++ b/Linphone/UI/Main/Conversations/ConversationsView.swift @@ -25,12 +25,21 @@ struct ConversationsView: View { @ObservedObject var conversationsListViewModel: ConversationsListViewModel @Binding var text: String + @Binding var isShowStartConversationFragment: Bool + var body: some View { NavigationView { ZStack(alignment: .bottomTrailing) { ConversationsFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, text: $text) Button { + withAnimation { + isShowStartConversationFragment = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + MagicSearchSingleton.shared.searchForSuggestions() + } } label: { Image("plus-circle") .renderingMode(.template) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift index 0e064b39a..3fe7b7f84 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift @@ -135,21 +135,25 @@ struct ConversationsListFragment: View { .listRowSeparator(.hidden) .background(.white) .onTapGesture { - if conversationViewModel.displayedConversation != nil { - conversationViewModel.displayedConversation = nil - conversationViewModel.resetMessage() - conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index]) - - conversationViewModel.getMessages() - } else { - withAnimation { + if index < conversationsListViewModel.conversationsList.count { + if conversationViewModel.displayedConversation != nil { + conversationViewModel.displayedConversation = nil + conversationViewModel.resetMessage() conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index]) + + conversationViewModel.getMessages() + } else { + withAnimation { + conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index]) + } } } } .onLongPressGesture(minimumDuration: 0.2) { - conversationsListViewModel.selectedConversation = conversationsListViewModel.conversationsList[index] - showingSheet.toggle() + if index < conversationsListViewModel.conversationsList.count { + conversationsListViewModel.selectedConversation = conversationsListViewModel.conversationsList[index] + showingSheet.toggle() + } } } } diff --git a/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift new file mode 100644 index 000000000..1e4ee9e2a --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift @@ -0,0 +1,397 @@ +/* + * 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 StartConversationFragment: View { + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var startConversationViewModel: StartConversationViewModel + @ObservedObject var conversationViewModel: ConversationViewModel + + @Binding var isShowStartConversationFragment: Bool + + @FocusState var isSearchFieldFocused: Bool + @State private var delayedColor = Color.white + + @FocusState var isMessageTextFocused: Bool + + @State var operationInProgress: Bool = false + + var body: some View { + NavigationView { + 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(.all, 10) + .padding(.top, 2) + .padding(.leading, -10) + .onTapGesture { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + + startConversationViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + delayColorDismiss() + withAnimation { + isShowStartConversationFragment = false + } + } + + Text("new_conversation_title") + .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("history_call_start_search_bar_filter_hint", text: $startConversationViewModel.searchField) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isSearchFieldFocused) + .padding(.horizontal, 30) + .onChange(of: startConversationViewModel.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 !startConversationViewModel.searchField.isEmpty { + Button(action: { + startConversationViewModel.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) + + NavigationLink(destination: { + //StartGroupConversationFragment(startConversationViewModel: startConversationViewModel) + }, label: { + HStack { + HStack(alignment: .center) { + Image("meetings") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 20, height: 20, alignment: .leading) + } + .padding(16) + .background(Color.orangeMain500) + .cornerRadius(40) + + Text("history_call_start_create_group_call") + .foregroundStyle(.black) + .default_text_style_800(styleSize: 16) + + Spacer() + + Image("caret-right") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + } + }) + .padding(.vertical, 10) + .padding(.horizontal, 20) + .background( + LinearGradient(gradient: Gradient(colors: [.grayMain2c100, .white]), startPoint: .leading, endPoint: .trailing) + .padding(.vertical, 10) + .padding(.horizontal, 40) + ) + + 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), startCallFunc: { addr in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue + ) + } + + startConversationViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + delayColorDismiss() + + withAnimation { + startConversationViewModel.createOneToOneChatRoomWith(remote: addr) + } + }) + .padding(.horizontal, 16) + + if !contactsManager.lastSearchSuggestions.isEmpty { + HStack(alignment: .center) { + Text("Suggestions") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } + } + } + .frame(maxWidth: .infinity) + } + .background(.white) + + if !startConversationViewModel.participants.isEmpty { + startConversationPopup + .background(.black.opacity(0.65)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + isMessageTextFocused = true + } + } + } + + if startConversationViewModel.operationInProgress { + PopupLoadingView() + .background(.black.opacity(0.65)) + .onDisappear { + isShowStartConversationFragment = false + + if startConversationViewModel.displayedConversation != nil { + if self.conversationViewModel.displayedConversation != nil { + self.conversationViewModel.displayedConversation = nil + self.conversationViewModel.resetMessage() + self.conversationViewModel.changeDisplayedChatRoom(conversationModel: startConversationViewModel.displayedConversation!) + + self.conversationViewModel.getMessages() + } else { + withAnimation { + self.conversationViewModel.changeDisplayedChatRoom(conversationModel: startConversationViewModel.displayedConversation!) + } + } + + startConversationViewModel.displayedConversation = nil + } + } + } + } + .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 +import Combine + +class StartConversationViewModel: ObservableObject { + + static let TAG = "[StartConversationViewModel]" + + private var coreContext = CoreContext.shared + + @Published var searchField: String = "" + + var domain: String = "" + + @Published var messageText: String = "" + + @Published var participants: [SelectedAddressModel] = [] + + @Published var operationInProgress: Bool = false + @Published var displayedConversation: ConversationModel? + + private var chatRoomSuscriptions = Set() + + init() { + coreContext.doOnCoreQueue { core in + self.domain = core.defaultAccount?.params?.domain ?? "" + } + } + + func addParticipants(participantsToAdd: [SelectedAddressModel]) { + var list = participants + for selectedAddr in participantsToAdd { + if let found = list.first(where: { $0.address.weakEqual(address2: selectedAddr.address) }) { + Log.info("\(StartConversationViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping") + continue + } + + list.append(selectedAddr) + Log.info("\(StartConversationViewModel.TAG) Added participant \(selectedAddr.address.asStringUriOnly())") + } + Log.info("\(StartConversationViewModel.TAG) [\(list.count - participants.count) participants added, now there are \(list.count) participants in list") + + participants = list + } + + /* + func createGroupChatRoom() { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + if (account == nil) { + Log.error( + "\(StartConversationViewModel.TAG) No default account found, can't create group conversation!" + ) + return + } + + operationInProgress = true + + let groupChatRoomSubject = subject + val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() + params.isGroupEnabled = true + params.subject = groupChatRoomSubject + params.backend = ChatRoom.Backend.FlexisipChat + params.isEncryptionEnabled = true + + val participants = arrayListOf
() + for (participant in selection.value.orEmpty()) { + participants.add(participant.address) + } + val localAddress = account.params.identityAddress + + val participantsArray = arrayOf
() + val chatRoom = core.createChatRoom( + params, + localAddress, + participants.toArray(participantsArray) + ) + if (chatRoom != null) { + if (params.backend == ChatRoom.Backend.FlexisipChat) { + if (chatRoom.state == ChatRoom.State.Created) { + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i( + "$TAG Group conversation [$id] ($groupChatRoomSubject) has been created" + ) + operationInProgress.postValue(false) + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } else { + Log.i( + "$TAG Conversation [$groupChatRoomSubject] isn't in Created state yet, wait for it" + ) + chatRoom.addListener(chatRoomListener) + } + } else { + val id = LinphoneUtils.getChatRoomId(chatRoom) + Log.i("$TAG Conversation successfully created [$id] ($groupChatRoomSubject)") + operationInProgress.postValue(false) + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + } + } else { + Log.e("$TAG Failed to create group conversation [$groupChatRoomSubject]!") + operationInProgress.postValue(false) + chatRoomCreationErrorEvent.postValue( + Event(R.string.conversation_failed_to_create_toast) + ) + } + } + } + */ + + func createOneToOneChatRoomWith(remote: Address) { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + if account == nil { + Log.error( + "\(StartConversationViewModel.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())!" + ) + return + } + + DispatchQueue.main.async { + self.operationInProgress = true + } + + do { + let params: ChatRoomParams = try core.createDefaultChatRoomParams() + params.groupEnabled = false + params.subject = "Dummy subject" + params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default + + let sameDomain = remote.domain == account?.params?.domain ?? "" + if self.isEndToEndEncryptionMandatory() && sameDomain { + Log.info("\(StartConversationViewModel.TAG) Account is in secure mode & domain matches, creating a E2E conversation") + params.backend = ChatRoom.Backend.FlexisipChat + params.encryptionEnabled = true + } else if !self.isEndToEndEncryptionMandatory() { + if LinphoneUtils.isEndToEndEncryptedChatAvailable(core: core) { + Log.info( + "\(StartConversationViewModel.TAG) Account is in interop mode but LIME is available, creating a E2E conversation" + ) + params.backend = ChatRoom.Backend.FlexisipChat + params.encryptionEnabled = true + } else { + Log.info( + "\(StartConversationViewModel.TAG) Account is in interop mode but LIME isn't available, creating a SIP simple conversation" + ) + params.backend = ChatRoom.Backend.Basic + params.encryptionEnabled = false + } + } else { + Log.error( + "\(StartConversationViewModel.TAG) Account is in secure mode, can't chat with SIP address of different domain \(remote.asStringUriOnly())" + ) + DispatchQueue.main.async { + self.operationInProgress = false + } + /* + chatRoomCreationErrorEvent.postValue( + Event(R.string.conversation_invalid_participant_due_to_security_mode_toast) + ) + */ + return + } + + let participants = [remote] + let localAddress = account?.params?.identityAddress + let existingChatRoom = core.searchChatRoom(params: params, localAddr: localAddress, remoteAddr: nil, participants: participants) + if existingChatRoom == nil { + Log.info( + "\(StartConversationViewModel.TAG) No existing 1-1 conversation between local account " + + "\(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) was found for given parameters, let's create it" + ) + let chatRoom = try core.createChatRoom(params: params, localAddr: localAddress, participants: participants) + if params.backend == ChatRoom.Backend.FlexisipChat { + if chatRoom.state == ChatRoom.State.Created { + let id = LinphoneUtils.getChatRoomId(room: chatRoom) + Log.info("\(StartConversationViewModel.TAG) 1-1 conversation \(id) has been created") + + /* + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + */ + } else { + Log.info("\(StartConversationViewModel.TAG) Conversation isn't in Created state yet, wait for it") + self.chatRoomAddDelegate(core: core, chatRoom: chatRoom) + } + } else { + let id = LinphoneUtils.getChatRoomId(room: chatRoom) + Log.info("\(StartConversationViewModel.TAG) Conversation successfully created \(id)") + + let model = ConversationModel(chatRoom: chatRoom) + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + /* + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + */ + } + } else { + Log.warn( + "\(StartConversationViewModel.TAG) A 1-1 conversation between local account \(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) for given parameters already exists!" + ) + + let model = ConversationModel(chatRoom: existingChatRoom!) + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + + + /* + chatRoomCreatedEvent.postValue( + Event( + Pair( + existingChatRoom.localAddress.asStringUriOnly(), + existingChatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + */ + } + } catch { + DispatchQueue.main.async { + self.operationInProgress = false + } + Log.error("\(StartConversationViewModel.TAG) Failed to create 1-1 conversation with \(remote.asStringUriOnly())!") + } + } + } + + func chatRoomAddDelegate(core: Core, chatRoom: ChatRoom) { + self.chatRoomSuscriptions.insert(chatRoom.publisher?.onConferenceJoined?.postOnCoreQueue { + (chatRoom: ChatRoom, eventLog: EventLog) in + let state = chatRoom.state + let id = LinphoneUtils.getChatRoomId(room: chatRoom) + Log.info("\(StartConversationViewModel.TAG) Conversation \(id) \(chatRoom.subject) state changed: \(state)") + if state == ChatRoom.State.Created { + Log.info("\(StartConversationViewModel.TAG) Conversation \(id) successfully created") + self.chatRoomSuscriptions.removeAll() + + DispatchQueue.main.async { + let model = ConversationModel(chatRoom: chatRoom) + self.operationInProgress = false + self.displayedConversation = model + } + + /* + chatRoomCreatedEvent.postValue( + Event( + Pair( + chatRoom.localAddress.asStringUriOnly(), + chatRoom.peerAddress.asStringUriOnly() + ) + ) + ) + */ + } else if state == ChatRoom.State.CreationFailed { + Log.error("\(StartConversationViewModel.TAG) Conversation \(id) creation has failed!") + self.chatRoomSuscriptions.removeAll() + DispatchQueue.main.async { + self.operationInProgress = false + } + /* + chatRoomCreationErrorEvent.postValue( + Event(R.string.conversation_failed_to_create_toast) + ) + */ + } + }) + } + + func isEndToEndEncryptionMandatory() -> Bool { + return false // TODO: Will be done later in SDK + } +} diff --git a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift index 3721884d8..1f11924e0 100644 --- a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift +++ b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift @@ -81,7 +81,7 @@ struct StartCallFragment: View { } } - Text(!callViewModel.isTransferInsteadCall ? "New call" : "Transfer call to") + Text(!callViewModel.isTransferInsteadCall ? "history_call_start_title" : "Transfer call to") .multilineTextAlignment(.leading) .default_text_style_orange_800(styleSize: 16) @@ -96,7 +96,7 @@ struct StartCallFragment: View { VStack(spacing: 0) { ZStack(alignment: .trailing) { - TextField("Search contact or history call", text: $startCallViewModel.searchField) + TextField("history_call_start_search_bar_filter_hint", text: $startCallViewModel.searchField) .default_text_style(styleSize: 15) .frame(height: 25) .focused($isSearchFieldFocused) @@ -266,16 +266,18 @@ struct StartCallFragment: View { }) .padding(.horizontal, 16) - HStack(alignment: .center) { - Text("Suggestions") - .default_text_style_800(styleSize: 16) + if !contactsManager.lastSearchSuggestions.isEmpty { + HStack(alignment: .center) { + Text("Suggestions") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) - Spacer() + suggestionsList } - .padding(.vertical, 10) - .padding(.horizontal, 16) - - suggestionsList } } .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/History/HistoryView.swift b/Linphone/UI/Main/History/HistoryView.swift index ee9f01b73..b60bbadba 100644 --- a/Linphone/UI/Main/History/HistoryView.swift +++ b/Linphone/UI/Main/History/HistoryView.swift @@ -47,9 +47,12 @@ struct HistoryView: View { Button { withAnimation { - MagicSearchSingleton.shared.searchForSuggestions() isShowStartCallFragment.toggle() } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + MagicSearchSingleton.shared.searchForSuggestions() + } } label: { Image("phone-plus") .renderingMode(.template) diff --git a/Linphone/Utils/LinphoneUtils.swift b/Linphone/Utils/LinphoneUtils.swift index 9b6a4e065..d959bbf0e 100644 --- a/Linphone/Utils/LinphoneUtils.swift +++ b/Linphone/Utils/LinphoneUtils.swift @@ -64,4 +64,9 @@ class LinphoneUtils: NSObject { return account?.params?.useInternationalPrefixForCallsAndChats == true || core.defaultAccount?.params?.useInternationalPrefixForCallsAndChats == true } + public class func isEndToEndEncryptedChatAvailable(core: Core) -> Bool { + return core.limeX3DhEnabled && + core.defaultAccount?.params?.limeServerUrl != nil && + core.defaultAccount?.params?.conferenceFactoryUri != nil + } }