From 76d4a8cdb37059d3306b919958aba5e1919771b5 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 15 Jul 2024 16:10:42 +0200 Subject: [PATCH] Access to conversation from a call --- Linphone/UI/Call/CallView.swift | 111 ++++++++-- .../UI/Call/ViewModel/CallViewModel.swift | 197 ++++++++++++++++++ Linphone/UI/Main/ContentView.swift | 11 +- .../Fragments/ConversationFragment.swift | 9 +- 4 files changed, 305 insertions(+), 23 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 4df149dc0..a5895da25 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -33,6 +33,8 @@ struct CallView: View { @ObservedObject private var contactsManager = ContactsManager.shared @ObservedObject var callViewModel: CallViewModel + @ObservedObject var conversationViewModel: ConversationViewModel + @ObservedObject var conversationsListViewModel: ConversationsListViewModel @State private var addParticipantsViewModel: AddParticipantsViewModel? @@ -60,6 +62,7 @@ struct CallView: View { @State var isShowCallsListFragment: Bool = false @State var isShowParticipantsListFragment: Bool = false @Binding var isShowStartCallFragment: Bool + @Binding var isShowConversationFragment: Bool @State var buttonSize = 60.0 @@ -187,6 +190,19 @@ 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 + } + } + if callViewModel.zrtpPopupDisplayed == true { if idiom != .pad && (orientation == .landscapeLeft @@ -2156,20 +2172,49 @@ struct CallView: View { HStack(spacing: 0) { VStack { Button { + if callViewModel.isOneOneCall && callViewModel.remoteAddress != nil { + callViewModel.createOneToOneChatRoomWith(remote: callViewModel.remoteAddress!) + } } label: { HStack { - Image("chat-teardrop-text") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.gray500) - .frame(width: 32, height: 32) + if !callViewModel.operationInProgress { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + .foregroundStyle(callViewModel.isOneOneCall ? .white : Color.gray500) + .frame(width: 32, height: 32) + } else { + ProgressView() + .controlSize(.mini) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(width: 32, height: 32, alignment: .center) + .onDisappear { + if callViewModel.isOneOneCall && callViewModel.displayedConversation != nil { + if conversationViewModel.displayedConversation != nil { + conversationViewModel.displayedConversation = nil + conversationViewModel.resetMessage() + conversationViewModel.changeDisplayedChatRoom(conversationModel: callViewModel.displayedConversation!) + + conversationViewModel.getMessages() + withAnimation { + isShowConversationFragment = true + } + } else { + conversationViewModel.changeDisplayedChatRoom(conversationModel: callViewModel.displayedConversation!) + withAnimation { + isShowConversationFragment = true + } + } + } + } + } } } .buttonStyle(PressedButtonStyle(buttonSize: buttonSize)) .frame(width: buttonSize, height: buttonSize) - .background(.white) + .background(callViewModel.isOneOneCall ? Color.gray500 : .white) .cornerRadius(40) - .disabled(true) + .disabled(!callViewModel.isOneOneCall) Text("Messages") .foregroundStyle(.white) @@ -2510,20 +2555,49 @@ struct CallView: View { VStack { Button { + if callViewModel.isOneOneCall && callViewModel.remoteAddress != nil { + callViewModel.createOneToOneChatRoomWith(remote: callViewModel.remoteAddress!) + } } label: { HStack { - Image("chat-teardrop-text") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.gray500) - .frame(width: 32, height: 32) + if !callViewModel.operationInProgress { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + .foregroundStyle(callViewModel.isOneOneCall ? .white : Color.gray500) + .frame(width: 32, height: 32) + } else { + ProgressView() + .controlSize(.mini) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(width: 32, height: 32, alignment: .center) + .onDisappear { + if callViewModel.isOneOneCall && callViewModel.displayedConversation != nil { + if conversationViewModel.displayedConversation != nil { + conversationViewModel.displayedConversation = nil + conversationViewModel.resetMessage() + conversationViewModel.changeDisplayedChatRoom(conversationModel: callViewModel.displayedConversation!) + + conversationViewModel.getMessages() + withAnimation { + isShowConversationFragment = true + } + } else { + conversationViewModel.changeDisplayedChatRoom(conversationModel: callViewModel.displayedConversation!) + withAnimation { + isShowConversationFragment = true + } + } + } + } + } } } .buttonStyle(PressedButtonStyle(buttonSize: buttonSize)) .frame(width: buttonSize, height: buttonSize) - .background(.white) + .background(callViewModel.isOneOneCall ? Color.gray500 : .white) .cornerRadius(40) - .disabled(true) + .disabled(!callViewModel.isOneOneCall) Text("Messages") .foregroundStyle(.white) @@ -2725,7 +2799,14 @@ struct PressedButtonStyle: ButtonStyle { } #Preview { - CallView(callViewModel: CallViewModel(), fullscreenVideo: .constant(false), isShowStartCallFragment: .constant(false)) + CallView( + callViewModel: CallViewModel(), + conversationViewModel: ConversationViewModel(), + conversationsListViewModel: ConversationsListViewModel(), + fullscreenVideo: .constant(false), + isShowStartCallFragment: .constant(false), + isShowConversationFragment: .constant(false) + ) } // swiftlint:enable type_body_length // swiftlint:enable line_length diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 6782337b0..50e57d7a1 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -81,6 +81,11 @@ class CallViewModel: ObservableObject { @Published var letters3: String = "CC" @Published var letters4: String = "DD" + @Published var operationInProgress: Bool = false + @Published var displayedConversation: ConversationModel? + + private var chatRoomSuscriptions = Set() + init() { do { try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth) @@ -1163,5 +1168,197 @@ class CallViewModel: ObservableObject { Log.info("\(CallViewModel.TAG) \(list.count) participants added to conference") } + + 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: 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 + } + } + }) + } } // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 028208ce9..c5ccb47f8 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -65,6 +65,7 @@ struct ContentView: View { @State var isShowSendCancelMeetingNotificationPopup = false @State var isShowSipAddressesPopup = false @State var isShowSipAddressesPopupType = 0 //0 to call, 1 to message, 2 to video call + @State var isShowConversationFragment = false @State var fullscreenVideo = false @@ -77,7 +78,7 @@ struct ContentView: View { GeometryReader { geometry in VStack(spacing: 0) { - if telecomManager.callInProgress && !fullscreenVideo && ((!telecomManager.callDisplayed && callViewModel.callsCounter == 1) || callViewModel.callsCounter > 1) { + if (telecomManager.callInProgress && !fullscreenVideo && ((!telecomManager.callDisplayed && callViewModel.callsCounter == 1) || callViewModel.callsCounter > 1)) || isShowConversationFragment { HStack { Image("phone") .renderingMode(.template) @@ -771,7 +772,7 @@ struct ContentView: View { .background(Color.gray100) .ignoresSafeArea(.keyboard) } else if self.index == 1 { - if historyViewModel.displayedCall!.avatarModel != nil { + if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.avatarModel != nil { HistoryContactFragment( contactAvatarModel: historyViewModel.displayedCall!.avatarModel!, historyViewModel: historyViewModel, @@ -787,7 +788,7 @@ struct ContentView: View { .ignoresSafeArea(.keyboard) } } else if self.index == 2 { - ConversationFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel) + ConversationFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, isShowConversationFragment: $isShowConversationFragment) .frame(maxWidth: .infinity) .background(Color.gray100) .ignoresSafeArea(.keyboard) @@ -1034,14 +1035,12 @@ struct ContentView: View { .onDisappear { if contactViewModel.displayedConversation != nil { contactViewModel.indexDisplayedFriend = nil - historyViewModel.displayedCall = nil index = 2 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { withAnimation { self.conversationViewModel.changeDisplayedChatRoom(conversationModel: contactViewModel.displayedConversation!) } contactViewModel.displayedConversation = nil - historyViewModel.displayedConversation = nil } } else if historyViewModel.displayedConversation != nil { historyViewModel.displayedCall = nil @@ -1105,7 +1104,7 @@ struct ContentView: View { } if telecomManager.callDisplayed && ((telecomManager.callInProgress && telecomManager.outgoingCallStarted) || telecomManager.callConnected) && !telecomManager.meetingWaitingRoomDisplayed { - CallView(callViewModel: callViewModel, fullscreenVideo: $fullscreenVideo, isShowStartCallFragment: $isShowStartCallFragment) + CallView(callViewModel: callViewModel, conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, fullscreenVideo: $fullscreenVideo, isShowStartCallFragment: $isShowStartCallFragment, isShowConversationFragment: $isShowConversationFragment) .zIndex(5) .transition(.scale.combined(with: .move(edge: .top))) .onAppear { diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 1180f7a6a..a8dcf144a 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -49,6 +49,8 @@ struct ConversationFragment: View { @State private var mediasIsLoading = false + @Binding var isShowConversationFragment: Bool + var body: some View { NavigationView { GeometryReader { geometry in @@ -60,8 +62,8 @@ struct ConversationFragment: View { .frame(height: 0) HStack { - if !(orientation == .landscapeLeft || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + if (!(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height)) || isShowConversationFragment { Image("caret-left") .renderingMode(.template) .resizable() @@ -72,6 +74,9 @@ struct ConversationFragment: View { .padding(.leading, -10) .onTapGesture { withAnimation { + if isShowConversationFragment { + isShowConversationFragment = false + } conversationViewModel.displayedConversation = nil } }