diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index 0d7139532..bc127ce8b 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -26,10 +26,12 @@ struct ContactFragment: View { @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var conversationViewModel: ConversationViewModel @Binding var isShowDeletePopup: Bool @Binding var isShowDismissPopup: Bool @Binding var isShowSipAddressesPopup: Bool + @Binding var isShowSipAddressesPopupType: Int @State private var showingSheet = false @State private var showShareSheet = false @@ -42,12 +44,14 @@ struct ContactFragment: View { contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + conversationViewModel: conversationViewModel, cnContact: CNContact(), isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet, showShareSheet: $showShareSheet, isShowDismissPopup: $isShowDismissPopup, - isShowSipAddressesPopup: $isShowSipAddressesPopup + isShowSipAddressesPopup: $isShowSipAddressesPopup, + isShowSipAddressesPopupType: $isShowSipAddressesPopupType ) .sheet(isPresented: $showingSheet) { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) @@ -63,12 +67,14 @@ struct ContactFragment: View { contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + conversationViewModel: conversationViewModel, cnContact: CNContact(), isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet, showShareSheet: $showShareSheet, isShowDismissPopup: $isShowDismissPopup, - isShowSipAddressesPopup: $isShowSipAddressesPopup + isShowSipAddressesPopup: $isShowSipAddressesPopup, + isShowSipAddressesPopupType: $isShowSipAddressesPopupType ) .halfSheet(showSheet: $showingSheet) { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) @@ -86,8 +92,10 @@ struct ContactFragment: View { ContactFragment( contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), + conversationViewModel: ConversationViewModel(), isShowDeletePopup: .constant(false), isShowDismissPopup: .constant(false), - isShowSipAddressesPopup: .constant(false) + isShowSipAddressesPopup: .constant(false), + isShowSipAddressesPopupType: .constant(0) ) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index 01104363c..c1b97c9e1 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -31,6 +31,7 @@ struct ContactInnerFragment: View { @ObservedObject var contactAvatarModel: ContactAvatarModel @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var conversationViewModel: ConversationViewModel @State private var orientation = UIDevice.current.orientation @@ -42,6 +43,7 @@ struct ContactInnerFragment: View { @Binding var showShareSheet: Bool @Binding var isShowDismissPopup: Bool @Binding var isShowSipAddressesPopup: Bool + @Binding var isShowSipAddressesPopupType: Int var body: some View { NavigationView { @@ -161,6 +163,7 @@ struct ContactInnerFragment: View { Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") } } else { + isShowSipAddressesPopupType = 0 isShowSipAddressesPopup = true } }, label: { @@ -184,21 +187,25 @@ struct ContactInnerFragment: View { Spacer() Button(action: { - + if contactAvatarModel.addresses.count <= 1 { + do { + let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address) + contactViewModel.createOneToOneChatRoomWith(remote: address) + } catch { + Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") + } + } else { + isShowSipAddressesPopupType = 1 + isShowSipAddressesPopup = true + } }, label: { VStack { HStack(alignment: .center) { Image("chat-teardrop-text") .renderingMode(.template) .resizable() - //.foregroundStyle(Color.grayMain2c600) - .foregroundStyle(Color.grayMain2c300) + .foregroundStyle(Color.grayMain2c600) .frame(width: 25, height: 25) - .onTapGesture { - withAnimation { - - } - } } .padding(16) .background(Color.grayMain2c200) @@ -220,6 +227,7 @@ struct ContactInnerFragment: View { Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") } } else { + isShowSipAddressesPopupType = 2 isShowSipAddressesPopup = true } }, label: { @@ -296,69 +304,6 @@ struct ContactInnerFragment: View { print(error) } } - - var sipAddressesPopup: some View { - GeometryReader { geometry in - VStack(alignment: .leading) { - HStack { - Text("contact_dialog_pick_phone_number_or_sip_address_title") - .default_text_style_800(styleSize: 16) - .background(.red) - .padding(.bottom, 2) - - Spacer() - - Image("x") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25) - .padding(.all, 10) - } - .frame(maxWidth: .infinity) - - ForEach(0..() + init() {} + + 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 + } + } + }) + } } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 307bae4d9..95a09f43f 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -64,6 +64,7 @@ struct ContentView: View { @State var isShowDismissPopup = false @State var isShowSendCancelMeetingNotificationPopup = false @State var isShowSipAddressesPopup = false + @State var isShowSipAddressesPopupType = 0 //0 to call, 1 to message, 2 to video call @State var fullscreenVideo = false @@ -760,9 +761,11 @@ struct ContentView: View { ContactFragment( contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + conversationViewModel: conversationViewModel, isShowDeletePopup: $isShowDeleteContactPopup, isShowDismissPopup: $isShowDismissPopup, - isShowSipAddressesPopup: $isShowSipAddressesPopup + isShowSipAddressesPopup: $isShowSipAddressesPopup, + isShowSipAddressesPopupType: $isShowSipAddressesPopupType ) .frame(maxWidth: .infinity) .background(Color.gray100) @@ -1014,7 +1017,8 @@ struct ContentView: View { SipAddressesPopup( contactAvatarModel: ContactsManager.shared.avatarListModel[contactViewModel.indexDisplayedFriend != nil ? contactViewModel.indexDisplayedFriend! : 0], contactViewModel: contactViewModel, - isShowSipAddressesPopup: $isShowSipAddressesPopup + isShowSipAddressesPopup: $isShowSipAddressesPopup, + isShowSipAddressesPopupType: $isShowSipAddressesPopupType ) .background(.black.opacity(0.65)) .zIndex(3) @@ -1023,6 +1027,25 @@ struct ContentView: View { } } + if contactViewModel.operationInProgress { + PopupLoadingView() + .background(.black.opacity(0.65)) + .zIndex(3) + .onDisappear { + if contactViewModel.displayedConversation != nil { + contactViewModel.indexDisplayedFriend = nil + index = 2 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation { + self.conversationViewModel.changeDisplayedChatRoom(conversationModel: contactViewModel.displayedConversation!) + } + contactViewModel.displayedConversation = nil + } + + } + } + } + if isShowScheduleMeetingFragment { ScheduleMeetingFragment( meetingViewModel: meetingViewModel, diff --git a/Linphone/UI/Main/Conversations/ViewModel/StartConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/StartConversationViewModel.swift index 5c60bb45c..fec47193a 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/StartConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/StartConversationViewModel.swift @@ -182,11 +182,11 @@ class StartConversationViewModel: ObservableObject { params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default let sameDomain = remote.domain == account?.params?.domain ?? "" - if self.isEndToEndEncryptionMandatory() && sameDomain { + 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 !self.isEndToEndEncryptionMandatory() { + } 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" @@ -353,7 +353,7 @@ class StartConversationViewModel: ObservableObject { }) } - func isEndToEndEncryptionMandatory() -> Bool { + public static func isEndToEndEncryptionMandatory() -> Bool { return false // TODO: Will be done later in SDK } }