diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index e1fb6d0ba..64853448b 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -58,6 +58,8 @@ D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */; }; D70A26F02B7D02E6006CC8FC /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */; }; D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */; }; + D70C82A52C85EDCA0087F43F /* ConversationForwardMessageFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */; }; + D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70C82A62C85F5910087F43F /* ConversationForwardMessageViewModel.swift */; }; D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */; }; D714035B2BE11E00004BD8CA /* CallMediaEncryptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */; }; D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */; }; @@ -240,6 +242,8 @@ D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListBottomSheet.swift; sourceTree = ""; }; D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationFragment.swift; sourceTree = ""; }; + D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationForwardMessageFragment.swift; sourceTree = ""; }; + D70C82A62C85F5910087F43F /* ConversationForwardMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationForwardMessageViewModel.swift; sourceTree = ""; }; D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMediaEncryptionModel.swift; sourceTree = ""; }; D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewModel.swift; sourceTree = ""; }; @@ -826,6 +830,7 @@ D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */, D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */, D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */, + D70C82A62C85F5910087F43F /* ConversationForwardMessageViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -841,6 +846,7 @@ D72A9A042B9750A1000DC093 /* UIList.swift */, D759CB632C3FBD4200AC35E8 /* StartConversationFragment.swift */, D7A0ACBA2C415D630043AE79 /* StartGroupConversationFragment.swift */, + D70C82A42C85EDC90087F43F /* ConversationForwardMessageFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -1155,6 +1161,7 @@ D7A0ACBB2C415D630043AE79 /* StartGroupConversationFragment.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */, + D70C82A52C85EDCA0087F43F /* ConversationForwardMessageFragment.swift in Sources */, D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */, D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */, D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */, @@ -1190,6 +1197,7 @@ D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */, + D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */, D74DA0122C047F0700A8561D /* HistoryModel.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */, @@ -1239,6 +1247,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", + "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; @@ -1252,7 +1261,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 6.0.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 = ""; @@ -1278,7 +1287,10 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_USER_SCRIPT_SANDBOXING = NO; 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; @@ -1291,7 +1303,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 6.0.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 = ""; @@ -1438,6 +1450,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", + "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; @@ -1464,7 +1477,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; @@ -1490,7 +1503,10 @@ 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"; @@ -1516,7 +1532,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 c67122247..dce056b89 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -82,6 +82,7 @@ struct LinphoneApp: App { @State private var conversationViewModel: ConversationViewModel? @State private var meetingsListViewModel: MeetingsListViewModel? @State private var meetingViewModel: MeetingViewModel? + @State private var conversationForwardMessageViewModel: ConversationForwardMessageViewModel? var body: some Scene { WindowGroup { @@ -113,7 +114,8 @@ struct LinphoneApp: App { && conversationsListViewModel != nil && conversationViewModel != nil && meetingsListViewModel != nil - && meetingViewModel != nil { + && meetingViewModel != nil + && conversationForwardMessageViewModel != nil { ContentView( contactViewModel: contactViewModel!, editContactViewModel: editContactViewModel!, @@ -126,7 +128,8 @@ struct LinphoneApp: App { conversationsListViewModel: conversationsListViewModel!, conversationViewModel: conversationViewModel!, meetingsListViewModel: meetingsListViewModel!, - meetingViewModel: meetingViewModel! + meetingViewModel: meetingViewModel!, + conversationForwardMessageViewModel: conversationForwardMessageViewModel! ).onOpenURL { url in URIHandler.handleURL(url: url) } @@ -150,6 +153,7 @@ struct LinphoneApp: App { conversationViewModel = ConversationViewModel() meetingsListViewModel = MeetingsListViewModel() meetingViewModel = MeetingViewModel() + conversationForwardMessageViewModel = ConversationForwardMessageViewModel() }.onOpenURL { url in URIHandler.handleURL(url: url) } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index acc2a4b2a..2adeced6a 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -787,6 +787,9 @@ }, "Cancel for me only" : { + }, + "Cancelled" : { + }, "Ce mode vous permet d’être interopérable avec d’autres services SIP.\nVos communications seront chiffrées de point à point. " : { @@ -958,6 +961,23 @@ } } }, + "conversation_forward_message_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forward message to…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transférer à…" + } + } + } + }, "conversation_invalid_participant_due_to_security_mode_toast" : { "extractionState" : "manual", "localizations" : { @@ -975,6 +995,40 @@ } } }, + "conversation_message_forward_cancelled_toast" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Message forward was cancelled" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfert du message abandonné" + } + } + } + }, + "conversation_message_forwarded_toast" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Message was forwarded" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le message a été transféré" + } + } + } + }, "conversation_reply_to_message_title" : { "extractionState" : "manual", "localizations" : { @@ -1599,6 +1653,23 @@ }, "Message copied into clipboard" : { + }, + "message_forwarded_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forwarded" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transféré" + } + } + } }, "Messages" : { diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index d44ffa041..69def5681 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -36,6 +36,7 @@ struct CallView: View { @ObservedObject var callViewModel: CallViewModel @ObservedObject var conversationViewModel: ConversationViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel + @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel @State private var addParticipantsViewModel: AddParticipantsViewModel? @@ -192,16 +193,21 @@ struct CallView: View { } if isShowConversationFragment && conversationViewModel.displayedConversation != nil { - ConversationFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, isShowConversationFragment: $isShowConversationFragment) - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) - .zIndex(4) - .transition(.move(edge: .bottom)) - .onDisappear { - conversationViewModel.displayedConversation = nil - isShowConversationFragment = false - } + ConversationFragment( + conversationViewModel: conversationViewModel, + conversationsListViewModel: conversationsListViewModel, + conversationForwardMessageViewModel: conversationForwardMessageViewModel, + isShowConversationFragment: $isShowConversationFragment + ) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) + .zIndex(4) + .transition(.move(edge: .bottom)) + .onDisappear { + conversationViewModel.displayedConversation = nil + isShowConversationFragment = false + } } if callViewModel.zrtpPopupDisplayed == true { @@ -2799,6 +2805,7 @@ struct PressedButtonStyle: ButtonStyle { callViewModel: CallViewModel(), conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel(), + conversationForwardMessageViewModel: ConversationForwardMessageViewModel(), fullscreenVideo: .constant(false), isShowStartCallFragment: .constant(false), isShowConversationFragment: .constant(false) diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index d4858dc7e..e20188a02 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -46,6 +46,7 @@ struct ContentView: View { @ObservedObject var conversationViewModel: ConversationViewModel @ObservedObject var meetingsListViewModel: MeetingsListViewModel @ObservedObject var meetingViewModel: MeetingViewModel + @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel @State var index = 0 @State private var orientation = UIDevice.current.orientation @@ -800,7 +801,12 @@ struct ContentView: View { .ignoresSafeArea(.keyboard) } } else if self.index == 2 { - ConversationFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, isShowConversationFragment: $isShowConversationFragment) + ConversationFragment( + conversationViewModel: conversationViewModel, + conversationsListViewModel: conversationsListViewModel, + conversationForwardMessageViewModel: conversationForwardMessageViewModel, + isShowConversationFragment: $isShowConversationFragment + ) .frame(maxWidth: .infinity) .background(Color.gray100) .ignoresSafeArea(.keyboard) @@ -1107,21 +1113,29 @@ struct ContentView: View { } if telecomManager.callDisplayed && ((telecomManager.callInProgress && telecomManager.outgoingCallStarted) || telecomManager.callConnected) && !telecomManager.meetingWaitingRoomDisplayed { - CallView(callViewModel: callViewModel, conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, fullscreenVideo: $fullscreenVideo, isShowStartCallFragment: $isShowStartCallFragment, isShowConversationFragment: $isShowConversationFragment) - .zIndex(5) - .transition(.scale.combined(with: .move(edge: .top))) - .onAppear { - UIApplication.shared.isIdleTimerDisabled = true - callViewModel.resetCallView() - if callViewModel.callsCounter >= 1 { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - callViewModel.resetCallView() - } + CallView( + callViewModel: callViewModel, + conversationViewModel: conversationViewModel, + conversationsListViewModel: conversationsListViewModel, + conversationForwardMessageViewModel: conversationForwardMessageViewModel, + fullscreenVideo: $fullscreenVideo, + isShowStartCallFragment: $isShowStartCallFragment, + isShowConversationFragment: $isShowConversationFragment + ) + .zIndex(5) + .transition(.scale.combined(with: .move(edge: .top))) + .onAppear { + UIApplication.shared.isIdleTimerDisabled = true + callViewModel.resetCallView() + if callViewModel.callsCounter >= 1 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + callViewModel.resetCallView() } } - .onDisappear { - UIApplication.shared.isIdleTimerDisabled = false - } + } + .onDisappear { + UIApplication.shared.isIdleTimerDisabled = false + } } ToastView() @@ -1176,7 +1190,8 @@ struct ContentView: View { conversationsListViewModel: ConversationsListViewModel(), conversationViewModel: ConversationViewModel(), meetingsListViewModel: MeetingsListViewModel(), - meetingViewModel: MeetingViewModel() + meetingViewModel: MeetingViewModel(), + conversationForwardMessageViewModel: ConversationForwardMessageViewModel() ) } // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index e12e8e29b..c7b36fd29 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -68,6 +68,31 @@ struct ChatBubbleView: View { .padding(.bottom, 2) } + if eventLogMessage.message.isForward { + HStack { + if eventLogMessage.message.isOutgoing { + Spacer() + } + + VStack(alignment: eventLogMessage.message.isOutgoing ? .trailing : .leading, spacing: 0) { + HStack { + Image("forward") + .resizable() + .frame(width: 15, height: 15, alignment: .leading) + + Text("message_forwarded_label") + .default_text_style(styleSize: 12) + } + .padding(.bottom, 2) + } + + if !eventLogMessage.message.isOutgoing { + Spacer() + } + } + .frame(maxWidth: .infinity) + } + if eventLogMessage.message.replyMessage != nil { HStack { if eventLogMessage.message.isOutgoing { diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift new file mode 100644 index 000000000..d21972d96 --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift @@ -0,0 +1,329 @@ +/* + * 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 ConversationForwardMessageFragment: View { + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var conversationViewModel: ConversationViewModel + @ObservedObject var conversationsListViewModel: ConversationsListViewModel + @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel + + @Binding var isShowConversationForwardMessageFragment: Bool + + @FocusState var isSearchFieldFocused: Bool + @State private var delayedColor = Color.white + + @FocusState var isMessageTextFocused: Bool + + var body: some View { + NavigationView { + ZStack { + VStack(spacing: 1) { + + Rectangle() + .foregroundStyle(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + 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.3) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + + conversationForwardMessageViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + + conversationForwardMessageViewModel.selectedMessage = nil + withAnimation { + isShowConversationForwardMessageFragment = false + } + } + + Text("conversation_forward_message_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: $conversationForwardMessageViewModel.searchField) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isSearchFieldFocused) + .padding(.horizontal, 30) + .onChange(of: conversationForwardMessageViewModel.searchField) { newValue in + if newValue.isEmpty { + conversationForwardMessageViewModel.resetFilterConversations() + } else { + conversationForwardMessageViewModel.filterConversations() + } + 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 !conversationForwardMessageViewModel.searchField.isEmpty { + Button(action: { + conversationForwardMessageViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + conversationForwardMessageViewModel.resetFilterConversations() + 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 !conversationForwardMessageViewModel.conversationsList.isEmpty { + HStack(alignment: .center) { + Text("Conversations") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + conversationsList + } + + 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 + withAnimation { + conversationForwardMessageViewModel.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 conversationForwardMessageViewModel.operationInProgress { + PopupLoadingView() + .background(.black.opacity(0.65)) + .onDisappear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue + ) + } + + conversationForwardMessageViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + + conversationForwardMessageViewModel.forwardMessage() + + isShowConversationForwardMessageFragment = false + + if conversationForwardMessageViewModel.displayedConversation != nil { + if conversationViewModel.displayedConversation != nil { + conversationViewModel.displayedConversation = nil + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + conversationViewModel.selectedMessage = nil + conversationViewModel.resetMessage() + withAnimation { + self.conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationForwardMessageViewModel.displayedConversation!) + } + conversationViewModel.getMessages() + } + } else { + conversationViewModel.selectedMessage = nil + withAnimation { + self.conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationForwardMessageViewModel.displayedConversation!) + } + } + } + } + } + } + .navigationTitle("") + .navigationBarHidden(true) + .onDisappear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + + conversationForwardMessageViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + + conversationForwardMessageViewModel.selectedMessage = nil + withAnimation { + isShowConversationForwardMessageFragment = false + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } + + var conversationsList: some View { + ForEach(0... + */ + +import linphonesw +import Combine + +// swiftlint:disable line_length +class ConversationForwardMessageViewModel: ObservableObject { + + static let TAG = "[ConversationForwardMessageViewModel]" + + @Published var searchField: String = "" + + @Published var operationInProgress: Bool = false + + @Published var selectedMessage: EventLogMessage? + + @Published var conversationsList: [ConversationModel] = [] + var conversationsListTmp: [ConversationModel] = [] + + @Published var displayedConversation: ConversationModel? + + private var chatRoomSuscriptions = Set() + + init() {} + + func initConversationsLists(convsList: [ConversationModel]) { + conversationsListTmp = convsList + conversationsList = convsList + searchField = "" + operationInProgress = false + selectedMessage = nil + } + + func filterConversations() { + conversationsList.removeAll() + conversationsListTmp.forEach { conversation in + if conversation.subject.lowercased().contains(searchField.lowercased()) || !conversation.participantsAddress.filter({ $0.lowercased().contains(searchField.lowercased()) }).isEmpty { + conversationsList.append(conversation) + } + } + } + + func resetFilterConversations() { + conversationsList = conversationsListTmp + } + + func changeChatRoom(model: ConversationModel) { + if self.operationInProgress == false { + DispatchQueue.main.async { + self.operationInProgress = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.operationInProgress = false + self.displayedConversation = model + } + } else { + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + } + } + + func createOneToOneChatRoomWith(remote: Address) { + CoreContext.shared.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 StartConversationViewModel.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 !StartConversationViewModel.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 + ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_invalid_participant_error" + ToastViewModel.shared.displayToast = true + } + 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") + + let model = ConversationModel(chatRoom: chatRoom) + if self.operationInProgress == false { + DispatchQueue.main.async { + self.operationInProgress = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.operationInProgress = false + self.displayedConversation = model + } + } else { + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + } + } 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) + if self.operationInProgress == false { + DispatchQueue.main.async { + self.operationInProgress = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.operationInProgress = false + self.displayedConversation = model + } + } else { + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + } + } + } 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!) + if self.operationInProgress == false { + DispatchQueue.main.async { + self.operationInProgress = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.operationInProgress = false + self.displayedConversation = model + } + } else { + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + } + } + } catch { + DispatchQueue.main.async { + self.operationInProgress = false + ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error" + ToastViewModel.shared.displayToast = true + } + 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) 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() + + let model = ConversationModel(chatRoom: chatRoom) + if self.operationInProgress == false { + DispatchQueue.main.async { + self.operationInProgress = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.operationInProgress = false + self.displayedConversation = model + } + } else { + DispatchQueue.main.async { + self.operationInProgress = false + self.displayedConversation = model + } + } + } else if state == ChatRoom.State.CreationFailed { + Log.error("\(StartConversationViewModel.TAG) Conversation \(id) creation has failed!") + self.chatRoomSuscriptions.removeAll() + DispatchQueue.main.async { + self.operationInProgress = false + ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error" + ToastViewModel.shared.displayToast = true + } + } + }) + + self.chatRoomSuscriptions.insert(chatRoom.publisher?.onStateChanged?.postOnCoreQueue { (chatRoom: ChatRoom, state: ChatRoom.State) in + let state = chatRoom.state + let id = LinphoneUtils.getChatRoomId(room: chatRoom) + if state == ChatRoom.State.CreationFailed { + Log.error("\(StartConversationViewModel.TAG) Conversation \(id) creation has failed!") + self.chatRoomSuscriptions.removeAll() + DispatchQueue.main.async { + self.operationInProgress = false + ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error" + ToastViewModel.shared.displayToast = true + } + } + }) + } + + func forwardMessage() { + CoreContext.shared.doOnCoreQueue { _ in + if self.displayedConversation != nil && self.selectedMessage != nil { + if let messageToForward = self.selectedMessage!.eventLog.chatMessage { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + do { + let forwardedMessage = try self.displayedConversation!.chatRoom.createForwardMessage(message: messageToForward) + Log.info("\(ConversationForwardMessageViewModel.TAG) Sending forwarded message") + forwardedMessage.send() + + } catch let error { + print("\(#function) - Failed to create forward message: \(error)") + } + + self.selectedMessage = nil + self.displayedConversation = nil + } + /* + showGreenToastEvent.postValue( + Event(Pair(R.string.conversation_message_forwarded_toast, R.drawable.forward)) + ) + */ + } + } + } + } +} + +// swiftlint:enable line_length diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 471779b47..621b93e29 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -70,6 +70,33 @@ class ConversationViewModel: ObservableObject { func addChatMessageDelegate(message: ChatMessage) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if self.displayedConversation != nil { + var statusTmp: Message.Status? = .sending + switch message.state { + case .InProgress: + statusTmp = .sending + case .Delivered: + statusTmp = .sent + case .DeliveredToUser: + statusTmp = .received + case .Displayed: + statusTmp = .read + case .NotDelivered: + statusTmp = .error + default: + statusTmp = .sending + } + + let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventLog.chatMessage?.messageId == message.messageId}) + + if self.conversationMessagesSection[0].rows[indexMessage!].message.status != statusTmp { + DispatchQueue.main.async { + if indexMessage != nil { + self.objectWillChange.send() + self.conversationMessagesSection[0].rows[indexMessage!].message.status = statusTmp + } + } + } + self.coreContext.doOnCoreQueue { _ in self.chatMessageSuscriptions.insert(message.publisher?.onMsgStateChanged?.postOnCoreQueue {(cbValue: (message: ChatMessage, state: ChatMessage.State)) in var statusTmp: Message.Status? = .sending @@ -381,6 +408,7 @@ class ConversationViewModel: ObservableObject { attachmentsNames: attachmentNameList, attachments: attachmentList, replyMessage: replyMessageTmp, + isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp ) @@ -581,6 +609,7 @@ class ConversationViewModel: ObservableObject { attachmentsNames: attachmentNameList, attachments: attachmentList, replyMessage: replyMessageTmp, + isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp ) @@ -793,6 +822,7 @@ class ConversationViewModel: ObservableObject { attachmentsNames: attachmentNameList, attachments: attachmentList, replyMessage: replyMessageTmp, + isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp ) @@ -1048,6 +1078,7 @@ class ConversationViewModel: ObservableObject { attachmentsNames: attachmentNameList, attachments: attachmentList, replyMessage: replyMessageTmp, + isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp )