Add media and document lists to the contact detail view

This commit is contained in:
Benoit Martins 2026-02-26 14:18:01 +01:00
parent 75e96ed8a5
commit bfb4ac3c22
10 changed files with 220 additions and 7 deletions

View file

@ -2,6 +2,6 @@ import Foundation
public enum AppGitInfo {
public static let branch = "master"
public static let commit = "9cc8923e3"
public static let commit = "75e96ed8a"
public static let tag = "6.1.0-alpha"
}

View file

@ -23,8 +23,9 @@ import linphonesw
// swiftlint:disable type_body_length
struct ContactInnerActionsFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject private var contactsManager = ContactsManager.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@EnvironmentObject var contactAvatarModel: ContactAvatarModel
@EnvironmentObject var contactsListViewModel: ContactsListViewModel
@ -37,6 +38,8 @@ struct ContactInnerActionsFragment: View {
@Binding var isShowDeletePopup: Bool
@Binding var isShowDismissPopup: Bool
@Binding var isShowTrustLevelPopup: Bool
@Binding var isShowMediaFilesFragment: Bool
@Binding var isShowDocumentsFilesFragment: Bool
@Binding var isShowIncreaseTrustLevelPopup: Bool
@Binding var isShowEditContactFragmentInContactDetails: Bool
@ -386,6 +389,87 @@ struct ContactInnerActionsFragment: View {
.transition(.move(edge: .top))
}
if sharedMainViewModel.displayedFriendExistingChatRoom != nil {
HStack(alignment: .center) {
Text("conversation_details_media_documents_title")
.default_text_style_800(styleSize: 16)
Spacer()
Image("caret-up")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.hidden()
}
.padding(.top, 20)
.padding(.bottom, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
VStack(spacing: 0) {
Button {
withAnimation {
isShowMediaFilesFragment = true
}
} label: {
HStack {
Image("image")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("conversation_menu_media_files")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
withAnimation {
isShowDocumentsFilesFragment = true
}
} label: {
HStack {
Image("file-pdf")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("conversation_menu_documents_files")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
HStack(alignment: .center) {
Text("contact_details_actions_title")
.default_text_style_800(styleSize: 16)

View file

@ -34,6 +34,8 @@ struct ContactInnerFragment: View {
@State var cnContact: CNContact?
@State private var presentingEditContact = false
@State private var isShowMediaFilesFragment = false
@State private var isShowDocumentsFilesFragment = false
@Binding var isShowDeletePopup: Bool
@Binding var showingSheet: Bool
@ -280,6 +282,8 @@ struct ContactInnerFragment: View {
isShowDeletePopup: $isShowDeletePopup,
isShowDismissPopup: $isShowDismissPopup,
isShowTrustLevelPopup: $isShowTrustLevelPopup,
isShowMediaFilesFragment: $isShowMediaFilesFragment,
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment,
isShowIncreaseTrustLevelPopup: $isShowIncreaseTrustLevelPopup,
isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails,
geometry: geometry,
@ -287,9 +291,18 @@ struct ContactInnerFragment: View {
)
.onAppear {
contactsListViewModel.fetchDevicesAndTrust()
contactsListViewModel.getOneToOneChatRoomWith()
}
.onChange(of: SharedMainViewModel.shared.displayedFriend?.id) { _ in
isShowMediaFilesFragment = false
isShowDocumentsFilesFragment = false
SharedMainViewModel.shared.displayedFriendExistingChatRoom = nil
contactsListViewModel.fetchDevicesAndTrust()
contactsListViewModel.getOneToOneChatRoomWith()
}
.onDisappear {
SharedMainViewModel.shared.displayedFriendExistingChatRoom = nil
}
}
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
@ -311,6 +324,22 @@ struct ContactInnerFragment: View {
.edgesIgnoringSafeArea(.vertical)
}
}
if isShowMediaFilesFragment {
ConversationMediaListFragment(
isShowMediaFilesFragment: $isShowMediaFilesFragment
)
.zIndex(5)
.transition(.move(edge: .trailing))
}
if isShowDocumentsFilesFragment {
ConversationDocumentsListFragment(
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment
)
.zIndex(5)
.transition(.move(edge: .trailing))
}
}
}
}

View file

@ -337,5 +337,102 @@ class ContactsListViewModel: ObservableObject {
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

View file

@ -21,7 +21,6 @@ import SwiftUI
import linphonesw
struct ConversationDocumentsListFragment: View {
@EnvironmentObject var conversationViewModel: ConversationViewModel
@StateObject private var conversationDocumentsListViewModel = ConversationDocumentsListViewModel()

View file

@ -1603,7 +1603,6 @@ struct ConversationFragment: View {
ConversationMediaListFragment(
isShowMediaFilesFragment: $isShowMediaFilesFragment
)
.environmentObject(conversationViewModel)
.zIndex(5)
.transition(.move(edge: .trailing))
}
@ -1612,7 +1611,6 @@ struct ConversationFragment: View {
ConversationDocumentsListFragment(
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment
)
.environmentObject(conversationViewModel)
.zIndex(5)
.transition(.move(edge: .trailing))
}

View file

@ -21,8 +21,6 @@ import SwiftUI
import linphonesw
struct ConversationMediaListFragment: View {
@EnvironmentObject var conversationViewModel: ConversationViewModel
@StateObject private var conversationMediaListViewModel = ConversationMediaListViewModel()
@Binding var isShowMediaFilesFragment: Bool

View file

@ -36,6 +36,9 @@ final class ConversationDocumentsListViewModel: ObservableObject {
if let conversationModelTmp = SharedMainViewModel.shared.displayedConversation {
self.conversationModel = conversationModelTmp
loadDocumentsList()
} else if let conversationModelTmp = SharedMainViewModel.shared.displayedFriendExistingChatRoom {
self.conversationModel = conversationModelTmp
loadDocumentsList()
}
}

View file

@ -36,6 +36,9 @@ final class ConversationMediaListViewModel: ObservableObject {
if let conversationModelTmp = SharedMainViewModel.shared.displayedConversation {
self.conversationModel = conversationModelTmp
loadMediaList()
} else if let conversationModelTmp = SharedMainViewModel.shared.displayedFriendExistingChatRoom {
self.conversationModel = conversationModelTmp
loadMediaList()
}
}

View file

@ -45,6 +45,8 @@ class SharedMainViewModel: ObservableObject {
@Published var displayedConversation: ConversationModel?
@Published var displayedMeeting: MeetingModel?
@Published var displayedFriendExistingChatRoom: ConversationModel?
@Published var dialPlansList: [DialPlan?] = []
@Published var dialPlansLabelList: [String] = []
@Published var dialPlansShortLabelList: [String] = []