Access to conversation from a call

This commit is contained in:
Benoit Martins 2024-07-15 16:10:42 +02:00
parent 2e2dac6807
commit 76d4a8cdb3
4 changed files with 305 additions and 23 deletions

View file

@ -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

View file

@ -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<AnyCancellable?>()
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

View file

@ -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 {

View file

@ -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
}
}