/* * 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 linphonesw import Combine import SwiftUI // swiftlint:disable line_length class ContactsListViewModel: ObservableObject { static let TAG = "[ConversationForwardMessageViewModel]" @Published var selectedEditFriend: ContactAvatarModel? var stringToCopy: String = "" var selectedFriend: ContactAvatarModel? var selectedFriendToShare: ContactAvatarModel? var selectedFriendToDelete: ContactAvatarModel? @Published var devices: [ContactDeviceModel] = [] @Published var trustedDevicesPercentage: Double = 0.0 @Published var displayedConversation: ConversationModel? private var coreDelegate: CoreDelegate? private var contactChatRoomDelegate: ChatRoomDelegate? private let nativeAddressBookFriendList = "Native address-book" let linphoneAddressBookFriendList = "Linphone address-book" let tempRemoteAddressBookFriendList = "TempRemoteDirectoryContacts address-book" init() { addCoreDelegate() } func createOneToOneChatRoomWith(remote: Address) { CoreContext.shared.doOnCoreQueue { core in let account = core.defaultAccount if account == nil { Log.error( "\(Self.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())" ) return } DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = true } do { let params = try core.createConferenceParams(conference: nil) params.chatEnabled = true params.groupEnabled = false params.subject = NSLocalizedString("conversation_one_to_one_hidden_subject", comment: "") params.account = account guard let chatParams = params.chatParams else { return } chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default let sameDomain = remote.domain == AppServices.corePreferences.defaultDomain && remote.domain == account!.params?.domain if account!.params != nil && (account!.params!.instantMessagingEncryptionMandatory && sameDomain) { Log.info("\(ConversationForwardMessageViewModel.TAG) Account is in secure mode & domain matches, creating an E2E encrypted conversation") chatParams.backend = ChatRoom.Backend.FlexisipChat params.securityLevel = Conference.SecurityLevel.EndToEnd } else if account!.params != nil && (!account!.params!.instantMessagingEncryptionMandatory) { if LinphoneUtils.isEndToEndEncryptedChatAvailable(core: core) { Log.info( "\(ConversationForwardMessageViewModel.TAG) Account is in interop mode but LIME is available, creating an E2E encrypted conversation" ) chatParams.backend = ChatRoom.Backend.FlexisipChat params.securityLevel = Conference.SecurityLevel.EndToEnd } else { Log.info( "\(ConversationForwardMessageViewModel.TAG) Account is in interop mode but LIME isn't available, creating a SIP simple conversation" ) chatParams.backend = ChatRoom.Backend.Basic params.securityLevel = Conference.SecurityLevel.None } } else { Log.error( "\(ConversationForwardMessageViewModel.TAG) Account is in secure mode, can't chat with SIP address of different domain \(remote.asStringUriOnly())" ) DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = false ToastViewModel.shared.show("Failed_to_create_conversation_error") } 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( "\(ConversationForwardMessageViewModel.TAG) No existing 1-1 conversation between local account \(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) was found for given parameters, let's create it" ) do { let chatRoom = try core.createChatRoom(params: params, participants: participants) if chatParams.backend == ChatRoom.Backend.FlexisipChat { let state = chatRoom.state if state == ChatRoom.State.Created { let chatRoomId = LinphoneUtils.getConversationId(chatRoom: chatRoom) Log.info("\(ConversationForwardMessageViewModel.TAG) 1-1 conversation \(chatRoomId) has been created") let model = ConversationModel(chatRoom: chatRoom) DispatchQueue.main.async { self.displayedConversation = model SharedMainViewModel.shared.operationInProgress = false } } else { Log.info("\(ConversationForwardMessageViewModel.TAG) Conversation isn't in Created state yet (state is \(state)), wait for it") self.chatRoomAddDelegate(core: core, chatRoom: chatRoom) } } else { let chatRoomId = LinphoneUtils.getConversationId(chatRoom: chatRoom) Log.info("\(ConversationForwardMessageViewModel.TAG) Conversation successfully created \(chatRoomId)") let model = ConversationModel(chatRoom: chatRoom) DispatchQueue.main.async { self.displayedConversation = model SharedMainViewModel.shared.operationInProgress = false } } } catch { Log.error("\(ConversationForwardMessageViewModel.TAG) Failed to create 1-1 conversation with \(remote.asStringUriOnly())") DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = false ToastViewModel.shared.show("Failed_to_create_conversation_error") } } } else { Log.warn( "\(ConversationForwardMessageViewModel.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.displayedConversation = model SharedMainViewModel.shared.operationInProgress = false } } } catch { } } } func chatRoomAddDelegate(core: Core, chatRoom: ChatRoom) { contactChatRoomDelegate = ChatRoomDelegateStub(onStateChanged: { (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!") if let delegate = self.contactChatRoomDelegate { chatRoom.removeDelegate(delegate: delegate) self.contactChatRoomDelegate = nil } DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = false ToastViewModel.shared.show("Failed_to_create_conversation_error") } } }, onConferenceJoined: { (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") if let delegate = self.contactChatRoomDelegate { chatRoom.removeDelegate(delegate: delegate) self.contactChatRoomDelegate = nil } let model = ConversationModel(chatRoom: chatRoom) if SharedMainViewModel.shared.operationInProgress == false { DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { SharedMainViewModel.shared.operationInProgress = false self.displayedConversation = model } } else { DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = false self.displayedConversation = model } } } else if state == ChatRoom.State.CreationFailed { Log.error("\(StartConversationViewModel.TAG) Conversation \(id) creation has failed!") if let delegate = self.contactChatRoomDelegate { chatRoom.removeDelegate(delegate: delegate) self.contactChatRoomDelegate = nil } DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = false ToastViewModel.shared.show("Failed_to_create_conversation_error") } } }) chatRoom.addDelegate(delegate: contactChatRoomDelegate!) } func addCoreDelegate() { CoreContext.shared.doOnCoreQueue { core in if let coreDelegate = self.coreDelegate { core.removeDelegate(delegate: coreDelegate) self.coreDelegate = nil } self.coreDelegate = CoreDelegateStub( onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in if call.state == Call.State.End && SharedMainViewModel.shared.displayedFriend != nil { // Updates trust if need be DispatchQueue.main.async { self.fetchDevicesAndTrust() } } } ) if self.coreDelegate != nil { core.addDelegate(delegate: self.coreDelegate!) } } } func deleteSelectedContact() { CoreContext.shared.doOnCoreQueue { core in if self.selectedFriendToDelete != nil && self.selectedFriendToDelete!.friend != nil { if SharedMainViewModel.shared.displayedFriend != nil { DispatchQueue.main.async { withAnimation { SharedMainViewModel.shared.displayedFriend = nil } } } self.selectedFriendToDelete!.friend!.remove() } else if SharedMainViewModel.shared.displayedFriend != nil { DispatchQueue.main.async { withAnimation { SharedMainViewModel.shared.displayedFriend = nil } } SharedMainViewModel.shared.displayedFriend!.friend!.remove() } MagicSearchSingleton.shared.searchForContacts() } } func changeSelectedFriendToDelete() { if selectedFriend != nil { self.selectedFriendToDelete = self.selectedFriend } } func toggleStarredSelectedFriend() { CoreContext.shared.doOnCoreQueue { core in if let contactAvatar = self.selectedFriend, let friend = contactAvatar.friend { friend.edit() friend.starred.toggle() friend.done() let starredTmp = friend.starred DispatchQueue.main.async { contactAvatar.starred = starredTmp } } else if let displayedFriend = SharedMainViewModel.shared.displayedFriend, let friend = displayedFriend.friend { friend.edit() friend.starred.toggle() friend.done() let starredTmp = friend.starred DispatchQueue.main.async { displayedFriend.starred = starredTmp } } } } func fetchDevicesAndTrust() { if let friend = SharedMainViewModel.shared.displayedFriend?.friend { var devicesList: [ContactDeviceModel] = [] let friendDevices = friend.devices if friendDevices.isEmpty { Log.info("\(Self.TAG) No device found for friend [\(friend.name ?? "")]") } else { let devicesCount = friendDevices.count var trustedDevicesCount = 0 for device in friendDevices { let trusted = device.securityLevel == .EndToEndEncryptedAndVerified if let address = device.address { devicesList.append( ContactDeviceModel( name: device.displayName ?? NSLocalizedString("contact_device_without_name", comment: ""), address: address, trusted: trusted ) ) } if trusted { trustedDevicesCount += 1 } } if !devicesList.isEmpty { let percentage = trustedDevicesCount * 100 / devicesCount trustedDevicesPercentage = Double(percentage) } } devices = devicesList } } func getOneToOneChatRoomWith() { CoreContext.shared.doOnCoreQueue { core in if let contactAvatarModel = SharedMainViewModel.shared.displayedFriend { var remote: Address? if contactAvatarModel.addresses.count >= 1 { do { remote = try Factory.Instance.createAddress(addr: contactAvatarModel.address) } catch { Log.error("\(Self.TAG) unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error)") return } } else if contactAvatarModel.addresses.isEmpty && contactAvatarModel.phoneNumbersWithLabel.count == 1 { if let firstPhone = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl( url: firstPhone.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core) ) { remote = address } } guard let remote else { Log.error("\(Self.TAG) No valid remote address found") return } let account = core.defaultAccount if account == nil { Log.error( "\(Self.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())" ) return } do { let params = try core.createConferenceParams(conference: nil) params.chatEnabled = true params.groupEnabled = false params.subject = NSLocalizedString("conversation_one_to_one_hidden_subject", comment: "") params.account = account guard let chatParams = params.chatParams else { return } chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default let sameDomain = remote.domain == AppServices.corePreferences.defaultDomain && remote.domain == account!.params?.domain if account!.params != nil && (account!.params!.instantMessagingEncryptionMandatory && sameDomain) { Log.info("\(ConversationForwardMessageViewModel.TAG) Account is in secure mode & domain matches, creating an E2E encrypted conversation") chatParams.backend = ChatRoom.Backend.FlexisipChat params.securityLevel = Conference.SecurityLevel.EndToEnd } else if account!.params != nil && (!account!.params!.instantMessagingEncryptionMandatory) { if LinphoneUtils.isEndToEndEncryptedChatAvailable(core: core) { Log.info( "\(ConversationForwardMessageViewModel.TAG) Account is in interop mode but LIME is available, creating an E2E encrypted conversation" ) chatParams.backend = ChatRoom.Backend.FlexisipChat params.securityLevel = Conference.SecurityLevel.EndToEnd } else { Log.info( "\(ConversationForwardMessageViewModel.TAG) Account is in interop mode but LIME isn't available, creating a SIP simple conversation" ) chatParams.backend = ChatRoom.Backend.Basic params.securityLevel = Conference.SecurityLevel.None } } else { Log.error( "\(ConversationForwardMessageViewModel.TAG) Account is in secure mode, can't chat with SIP address of different domain \(remote.asStringUriOnly())" ) DispatchQueue.main.async { SharedMainViewModel.shared.operationInProgress = false ToastViewModel.shared.show("Failed_to_create_conversation_error") } return } let participants = [remote] let localAddress = account?.params?.identityAddress if let existingChatRoomTmp = core.searchChatRoom(params: params, localAddr: localAddress, remoteAddr: nil, participants: participants) { Log.warn( "\(ConversationForwardMessageViewModel.TAG) A 1-1 conversation between local account \(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) for given parameters already exists!" ) let conversationModel = ConversationModel(chatRoom: existingChatRoomTmp) DispatchQueue.main.async { SharedMainViewModel.shared.displayedFriendExistingChatRoom = conversationModel } } } catch { } } } } } // swiftlint:enable line_length