mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-04-17 20:08:31 +00:00
Add media and documents list to the conversation
This commit is contained in:
parent
9ac0445347
commit
8223d20fc6
11 changed files with 944 additions and 3 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public enum AppGitInfo {
|
||||
public static let branch = "master"
|
||||
public static let commit = "efed662a3"
|
||||
public static let branch = "feature/medias_and_documents_lists"
|
||||
public static let commit = "4dbdf455f"
|
||||
public static let tag = "6.1.0-alpha"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,6 +256,11 @@
|
|||
"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages";
|
||||
"conversation_menu_media_files" = "Media";
|
||||
"conversation_menu_documents_files" = "Documents";
|
||||
"conversation_no_media_found" = "No media found…";
|
||||
"conversation_no_document_found" = "No document found…";
|
||||
"conversation_media_list_title" = "Shared media";
|
||||
"conversation_document_list_title" = "Shared documents";
|
||||
"conversation_details_media_documents_title" = "Media & documents";
|
||||
"conversation_message_forward_cancelled_toast" = "Message forward was cancelled";
|
||||
"conversation_message_forwarded_toast" = "Message was forwarded";
|
||||
"conversation_message_meeting_cancelled_label" = "Meeting has been cancelled!";
|
||||
|
|
|
|||
|
|
@ -256,6 +256,11 @@
|
|||
"conversation_menu_configure_ephemeral_messages" = "Messages éphémères";
|
||||
"conversation_menu_media_files" = "Médias";
|
||||
"conversation_menu_documents_files" = "Documents";
|
||||
"conversation_no_media_found" = "Aucun média pour le moment…";
|
||||
"conversation_no_document_found" = "Aucun document pour le moment…";
|
||||
"conversation_media_list_title" = "Médias partagés";
|
||||
"conversation_document_list_title" = "Documents partagés";
|
||||
"conversation_details_media_documents_title" = "Médias & documents";
|
||||
"conversation_message_forward_cancelled_toast" = "Transfert annulé";
|
||||
"conversation_message_forwarded_toast" = "Message transféré";
|
||||
"conversation_message_meeting_cancelled_label" = "La réunion a été annulée";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import linphonesw
|
||||
|
||||
struct ConversationDocumentsListFragment: View {
|
||||
@EnvironmentObject var conversationViewModel: ConversationViewModel
|
||||
|
||||
@StateObject private var conversationDocumentsListViewModel = ConversationDocumentsListViewModel()
|
||||
|
||||
@Binding var isShowDocumentsFilesFragment: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
VStack(spacing: 1) {
|
||||
|
||||
Rectangle()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
|
||||
HStack {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 2)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isShowDocumentsFilesFragment = false
|
||||
}
|
||||
}
|
||||
|
||||
Text("conversation_document_list_title")
|
||||
.multilineTextAlignment(.leading)
|
||||
.default_text_style_orange_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
List {
|
||||
ForEach(conversationDocumentsListViewModel.documentsList, id: \.path) { file in
|
||||
MediaGridItemView(file: file)
|
||||
.background()
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .top, content: {
|
||||
Spacer()
|
||||
.frame(height: 12)
|
||||
})
|
||||
.listStyle(.plain)
|
||||
.overlay(
|
||||
VStack {
|
||||
if true {
|
||||
Spacer()
|
||||
Text("conversation_no_document_found")
|
||||
.multilineTextAlignment(.leading)
|
||||
.default_text_style_800(styleSize: 16)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.all)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(Color.gray100)
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
.onDisappear {
|
||||
withAnimation {
|
||||
isShowDocumentsFilesFragment = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentRow: View {
|
||||
|
||||
@ObservedObject var file: FileModel
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
if let previewPath = file.mediaPreview,
|
||||
let image = UIImage(contentsOfFile: previewPath) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(height: 110)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(height: 110)
|
||||
}
|
||||
|
||||
if let duration = file.audioVideoDuration, file.isVideoPreview {
|
||||
Text(duration)
|
||||
.font(.caption2)
|
||||
.padding(6)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,9 +59,11 @@ struct ConversationFragment: View {
|
|||
@State private var mediasIsLoading = false
|
||||
@State private var voiceRecordingInProgress = false
|
||||
|
||||
@State private var isShowConversationForwardMessageFragment = false
|
||||
@State private var isShowEphemeralFragment = false
|
||||
@State private var isShowMediaFilesFragment = false
|
||||
@State private var isShowDocumentsFilesFragment = false
|
||||
@State private var isShowInfoConversationFragment = false
|
||||
@State private var isShowConversationForwardMessageFragment = false
|
||||
|
||||
@Binding var isShowConversationFragment: Bool
|
||||
@Binding var isShowStartCallGroupPopup: Bool
|
||||
|
|
@ -456,6 +458,42 @@ struct ConversationFragment: View {
|
|||
.padding(.all, 10)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
isMenuOpen = false
|
||||
withAnimation {
|
||||
isShowMediaFilesFragment = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("conversation_menu_media_files")
|
||||
Spacer()
|
||||
Image("image")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
isMenuOpen = false
|
||||
withAnimation {
|
||||
isShowDocumentsFilesFragment = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("conversation_menu_documents_files")
|
||||
Spacer()
|
||||
Image("file-pdf")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image("dots-three-vertical")
|
||||
|
|
@ -1519,6 +1557,8 @@ struct ConversationFragment: View {
|
|||
ConversationInfoFragment(
|
||||
isMuted: $isMuted,
|
||||
isShowEphemeralFragment: $isShowEphemeralFragment,
|
||||
isShowMediaFilesFragment: $isShowMediaFilesFragment,
|
||||
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment,
|
||||
isShowStartCallGroupPopup: $isShowStartCallGroupPopup,
|
||||
isShowInfoConversationFragment: $isShowInfoConversationFragment,
|
||||
isShowEditContactFragment: $isShowEditContactFragment,
|
||||
|
|
@ -1543,6 +1583,24 @@ struct ConversationFragment: View {
|
|||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
|
||||
if isShowMediaFilesFragment {
|
||||
ConversationMediaListFragment(
|
||||
isShowMediaFilesFragment: $isShowMediaFilesFragment
|
||||
)
|
||||
.environmentObject(conversationViewModel)
|
||||
.zIndex(5)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
|
||||
if isShowDocumentsFilesFragment {
|
||||
ConversationDocumentsListFragment(
|
||||
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment
|
||||
)
|
||||
.environmentObject(conversationViewModel)
|
||||
.zIndex(5)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
|
||||
if conversationViewModel.searchInProgress {
|
||||
PopupLoadingView()
|
||||
.background(.black.opacity(0.65))
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ struct ConversationInfoFragment: View {
|
|||
|
||||
@Binding var isMuted: Bool
|
||||
@Binding var isShowEphemeralFragment: Bool
|
||||
@Binding var isShowMediaFilesFragment: Bool
|
||||
@Binding var isShowDocumentsFilesFragment: Bool
|
||||
@Binding var isShowStartCallGroupPopup: Bool
|
||||
@Binding var isShowInfoConversationFragment: Bool
|
||||
@Binding var isShowEditContactFragment: Bool
|
||||
|
|
@ -525,6 +527,69 @@ struct ConversationInfoFragment: View {
|
|||
}
|
||||
}
|
||||
|
||||
Text("conversation_details_media_documents_title")
|
||||
.default_text_style_800(styleSize: 18)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Button(
|
||||
action: {
|
||||
withAnimation {
|
||||
isShowMediaFilesFragment = true
|
||||
}
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Image("image")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
|
||||
Text("conversation_menu_media_files")
|
||||
.default_text_style(styleSize: 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(1)
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(height: 60)
|
||||
|
||||
Divider()
|
||||
|
||||
Button(
|
||||
action: {
|
||||
withAnimation {
|
||||
isShowDocumentsFilesFragment = true
|
||||
}
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Image("file-pdf")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
|
||||
Text("conversation_menu_documents_files")
|
||||
.default_text_style(styleSize: 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(1)
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(height: 60)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 4)
|
||||
.background(.white)
|
||||
.cornerRadius(15)
|
||||
.padding(.all)
|
||||
|
||||
Text("contact_details_actions_title")
|
||||
.default_text_style_800(styleSize: 18)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
@ -710,6 +775,8 @@ struct ConversationInfoFragment: View {
|
|||
ConversationInfoFragment(
|
||||
isMuted: .constant(false),
|
||||
isShowEphemeralFragment: .constant(false),
|
||||
isShowMediaFilesFragment: .constant(false),
|
||||
isShowDocumentsFilesFragment: .constant(false),
|
||||
isShowStartCallGroupPopup: .constant(false),
|
||||
isShowInfoConversationFragment: .constant(true),
|
||||
isShowEditContactFragment: .constant(false),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import linphonesw
|
||||
|
||||
struct ConversationMediaListFragment: View {
|
||||
@EnvironmentObject var conversationViewModel: ConversationViewModel
|
||||
|
||||
@StateObject private var conversationMediaListViewModel = ConversationMediaListViewModel()
|
||||
|
||||
@Binding var isShowMediaFilesFragment: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
VStack(spacing: 1) {
|
||||
|
||||
Rectangle()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
|
||||
HStack {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 2)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isShowMediaFilesFragment = false
|
||||
}
|
||||
}
|
||||
|
||||
Text("conversation_media_list_title")
|
||||
.multilineTextAlignment(.leading)
|
||||
.default_text_style_orange_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
ConversationMediaGridView(viewModel: conversationMediaListViewModel)
|
||||
}
|
||||
.background(Color.gray100)
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
.onDisappear {
|
||||
withAnimation {
|
||||
isShowMediaFilesFragment = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct ConversationMediaGridView: View {
|
||||
|
||||
@ObservedObject var viewModel: ConversationMediaListViewModel
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible(), spacing: 1),
|
||||
GridItem(.flexible(), spacing: 1),
|
||||
GridItem(.flexible(), spacing: 1)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.mediaList.isEmpty && !viewModel.operationInProgress {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 1) {
|
||||
ForEach(viewModel.mediaList, id: \.path) { file in
|
||||
MediaGridItemView(file: file)
|
||||
.onTapGesture {
|
||||
//viewModel.openMediaEvent.send(file)
|
||||
}
|
||||
.onAppear {
|
||||
if file == viewModel.mediaList.last {
|
||||
viewModel.loadMoreData(totalItemsCount: viewModel.mediaList.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 2)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
} else if viewModel.mediaList.isEmpty && !viewModel.operationInProgress {
|
||||
Spacer()
|
||||
Text("conversation_no_media_found")
|
||||
.multilineTextAlignment(.center)
|
||||
.default_text_style_800(styleSize: 16)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MediaGridItemView: View {
|
||||
|
||||
@ObservedObject var file: FileModel
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
if let previewPath = file.mediaPreview,
|
||||
let image = UIImage(contentsOfFile: previewPath) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 120, height: 120)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: 120, height: 120)
|
||||
}
|
||||
|
||||
if file.isVideoPreview {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 35, height: 35)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 120, height: 120)
|
||||
}
|
||||
|
||||
if let duration = file.audioVideoDuration, file.isVideoPreview {
|
||||
Text(duration)
|
||||
.font(.caption2)
|
||||
.padding(6)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
235
Linphone/UI/Main/Conversations/Model/FileModel.swift
Normal file
235
Linphone/UI/Main/Conversations/Model/FileModel.swift
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of Linphone
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import PDFKit
|
||||
|
||||
final class FileModel: ObservableObject, Equatable {
|
||||
|
||||
// MARK: - Inputs
|
||||
let path: String
|
||||
let fileName: String
|
||||
let fileSize: Int64
|
||||
let fileCreationTimestamp: Int64
|
||||
let isEncrypted: Bool
|
||||
let originalPath: String
|
||||
let isFromEphemeralMessage: Bool
|
||||
let isWaitingToBeDownloaded: Bool
|
||||
let flexboxLayoutWrapBefore: Bool
|
||||
private let onClicked: ((FileModel) -> Void)?
|
||||
|
||||
@Published var formattedFileSize: String = ""
|
||||
@Published var transferProgress: Int = -1
|
||||
@Published var transferProgressLabel: String = ""
|
||||
@Published var mediaPreview: String? = nil
|
||||
@Published var mediaPreviewAvailable: Bool = false
|
||||
@Published var audioVideoDuration: String? = nil
|
||||
|
||||
// MARK: - Computed
|
||||
let mimeTypeString: String
|
||||
let isMedia: Bool
|
||||
let isImage: Bool
|
||||
let isVideoPreview: Bool
|
||||
let isPdf: Bool
|
||||
let isAudio: Bool
|
||||
|
||||
let month: String
|
||||
let dateTime: String
|
||||
|
||||
static func == (lhs: FileModel, rhs: FileModel) -> Bool {
|
||||
return lhs.path == rhs.path
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
init(
|
||||
path: String,
|
||||
fileName: String,
|
||||
fileSize: Int64,
|
||||
fileCreationTimestamp: Int64,
|
||||
isEncrypted: Bool,
|
||||
originalPath: String,
|
||||
isFromEphemeralMessage: Bool,
|
||||
isWaitingToBeDownloaded: Bool = false,
|
||||
flexboxLayoutWrapBefore: Bool = false,
|
||||
onClicked: ((FileModel) -> Void)? = nil
|
||||
) {
|
||||
self.path = path
|
||||
self.fileName = fileName
|
||||
self.fileSize = fileSize
|
||||
self.fileCreationTimestamp = fileCreationTimestamp
|
||||
self.isEncrypted = isEncrypted
|
||||
self.originalPath = originalPath
|
||||
self.isFromEphemeralMessage = isFromEphemeralMessage
|
||||
self.isWaitingToBeDownloaded = isWaitingToBeDownloaded
|
||||
self.flexboxLayoutWrapBefore = flexboxLayoutWrapBefore
|
||||
self.onClicked = onClicked
|
||||
|
||||
let ext = (path as NSString).pathExtension.lowercased()
|
||||
self.isPdf = ext == "pdf"
|
||||
|
||||
let mime = FileModel.mimeType(from: ext)
|
||||
self.mimeTypeString = mime
|
||||
|
||||
self.isImage = mime.hasPrefix("image/")
|
||||
self.isVideoPreview = mime.hasPrefix("video/")
|
||||
self.isAudio = mime.hasPrefix("audio/")
|
||||
self.isMedia = isImage || isVideoPreview
|
||||
|
||||
self.month = FileModel.month(from: fileCreationTimestamp)
|
||||
self.dateTime = FileModel.formatDate(timestamp: fileCreationTimestamp)
|
||||
|
||||
computeFileSize(fileSize)
|
||||
updateTransferProgress(-1)
|
||||
|
||||
if !isWaitingToBeDownloaded {
|
||||
if isPdf { loadPdfPreview() }
|
||||
if isImage {
|
||||
mediaPreview = path
|
||||
mediaPreviewAvailable = true
|
||||
} else if isVideoPreview {
|
||||
loadVideoPreview()
|
||||
}
|
||||
if isVideoPreview || isAudio {
|
||||
getDuration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func onClick() {
|
||||
onClicked?(self)
|
||||
}
|
||||
|
||||
func destroy() {
|
||||
guard isEncrypted else { return }
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
try? FileManager.default.removeItem(atPath: self.path)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFile() async {
|
||||
try? FileManager.default.removeItem(atPath: path)
|
||||
}
|
||||
|
||||
func computeFileSize(_ size: Int64) {
|
||||
formattedFileSize = FileModel.bytesToReadable(size)
|
||||
}
|
||||
|
||||
func updateTransferProgress(_ percent: Int) {
|
||||
transferProgress = percent
|
||||
transferProgressLabel = (percent < 0 || percent > 100) ? "" : "\(percent)%"
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
private func loadPdfPreview() {
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
guard let pdf = PDFDocument(url: URL(fileURLWithPath: self.path)),
|
||||
let page = pdf.page(at: 0) else { return }
|
||||
|
||||
let pageRect = page.bounds(for: .mediaBox)
|
||||
let renderer = UIGraphicsImageRenderer(size: pageRect.size)
|
||||
let image = renderer.image { ctx in
|
||||
UIColor.white.set()
|
||||
ctx.fill(pageRect)
|
||||
page.draw(with: .mediaBox, to: ctx.cgContext)
|
||||
}
|
||||
|
||||
if let data = image.jpegData(compressionQuality: 0.8) {
|
||||
let url = FileModel.cacheFileURL(name: "\(self.fileName).jpg")
|
||||
try? data.write(to: url)
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPreview = url.path
|
||||
self.mediaPreviewAvailable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVideoPreview() {
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let asset = AVAsset(url: URL(fileURLWithPath: self.path))
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
|
||||
let time = CMTime(seconds: 1, preferredTimescale: 600)
|
||||
if let cgImage = try? generator.copyCGImage(at: time, actualTime: nil) {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
if let data = image.jpegData(compressionQuality: 0.8) {
|
||||
let url = FileModel.cacheFileURL(name: "\(self.fileName).jpg")
|
||||
try? data.write(to: url)
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPreview = url.path
|
||||
self.mediaPreviewAvailable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getDuration() {
|
||||
let asset = AVAsset(url: URL(fileURLWithPath: path))
|
||||
let seconds = Int(CMTimeGetSeconds(asset.duration))
|
||||
audioVideoDuration = FileModel.formatDuration(seconds)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private static func cacheFileURL(name: String) -> URL {
|
||||
let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
return dir.appendingPathComponent(name)
|
||||
}
|
||||
|
||||
private static func bytesToReadable(_ bytes: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: bytes)
|
||||
}
|
||||
|
||||
private static func mimeType(from ext: String) -> String {
|
||||
switch ext {
|
||||
case "jpg", "jpeg", "png", "heic": return "image/jpeg"
|
||||
case "mp4", "mov": return "video/mp4"
|
||||
case "mp3", "wav", "m4a": return "audio/mpeg"
|
||||
case "pdf": return "application/pdf"
|
||||
default: return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
private static func formatDuration(_ seconds: Int) -> String {
|
||||
let min = seconds / 60
|
||||
let sec = seconds % 60
|
||||
return String(format: "%02d:%02d", min, sec)
|
||||
}
|
||||
|
||||
private static func formatDate(timestamp: Int64) -> String {
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
private static func month(from timestamp: Int64) -> String {
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMMM"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import linphonesw
|
||||
|
||||
final class ConversationDocumentsListViewModel: ObservableObject {
|
||||
|
||||
private static let TAG = "[ConversationDocumentsListViewModel]"
|
||||
private static let CONTENTS_PER_PAGE = 20
|
||||
|
||||
@Published var documentsList: [FileModel] = []
|
||||
@Published var operationInProgress: Bool = false
|
||||
|
||||
private var totalDocumentsCount: Int = -1
|
||||
|
||||
private var conversationModel: ConversationModel!
|
||||
|
||||
init() {
|
||||
if let conversationModelTmp = SharedMainViewModel.shared.displayedConversation {
|
||||
self.conversationModel = conversationModelTmp
|
||||
loadDocumentsList()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading
|
||||
private func loadDocumentsList() {
|
||||
operationInProgress = true
|
||||
totalDocumentsCount = self.conversationModel.chatRoom.documentContentsSize
|
||||
|
||||
let contentsToLoad = min(totalDocumentsCount, Self.CONTENTS_PER_PAGE)
|
||||
let contents = self.conversationModel.chatRoom.getDocumentContentsRange(begin: 0, end: contentsToLoad)
|
||||
|
||||
documentsList = getFileModelsList(from: contents)
|
||||
operationInProgress = false
|
||||
}
|
||||
|
||||
func loadMoreData(totalItemsCount: Int) {
|
||||
guard totalItemsCount < totalDocumentsCount else { return }
|
||||
|
||||
var upperBound = totalItemsCount + Self.CONTENTS_PER_PAGE
|
||||
if upperBound > totalDocumentsCount {
|
||||
upperBound = totalDocumentsCount
|
||||
}
|
||||
|
||||
let contents = self.conversationModel.chatRoom.getDocumentContentsRange(begin: totalItemsCount, end: upperBound)
|
||||
let newModels = getFileModelsList(from: contents)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.documentsList.append(contentsOf: newModels)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mapping Content -> FileModel
|
||||
private func getFileModelsList(from contents: [Content]) -> [FileModel] {
|
||||
var list: [FileModel] = []
|
||||
|
||||
for documentContent in contents {
|
||||
let isEncrypted = documentContent.isFileEncrypted
|
||||
let originalPath = documentContent.filePath ?? ""
|
||||
let path = isEncrypted ? documentContent.exportPlainFile() : originalPath
|
||||
let name = documentContent.name ?? ""
|
||||
let size = Int64(documentContent.size)
|
||||
let timestamp = documentContent.creationTimestamp
|
||||
|
||||
if path.isEmpty || name.isEmpty { continue }
|
||||
|
||||
let ephemeral: Bool
|
||||
if let messageId = documentContent.relatedChatMessageId {
|
||||
if let chatMessage = self.conversationModel.chatRoom.findMessage(messageId: messageId) {
|
||||
ephemeral = chatMessage.isEphemeral
|
||||
} else {
|
||||
ephemeral = self.conversationModel.chatRoom.ephemeralEnabled
|
||||
}
|
||||
} else {
|
||||
ephemeral = self.conversationModel.chatRoom.ephemeralEnabled
|
||||
}
|
||||
|
||||
let model = FileModel(
|
||||
path: path,
|
||||
fileName: name,
|
||||
fileSize: size,
|
||||
fileCreationTimestamp: Int64(timestamp),
|
||||
isEncrypted: isEncrypted,
|
||||
originalPath: originalPath,
|
||||
isFromEphemeralMessage: ephemeral
|
||||
)
|
||||
|
||||
list.append(model)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import linphonesw
|
||||
|
||||
final class ConversationMediaListViewModel: ObservableObject {
|
||||
|
||||
private static let TAG = "[ConversationMediaListViewModel]"
|
||||
private static let CONTENTS_PER_PAGE = 50
|
||||
|
||||
@Published var mediaList: [FileModel] = []
|
||||
@Published var operationInProgress: Bool = false
|
||||
|
||||
private var totalMediaCount: Int = -1
|
||||
|
||||
private var conversationModel: ConversationModel!
|
||||
|
||||
init() {
|
||||
if let conversationModelTmp = SharedMainViewModel.shared.displayedConversation {
|
||||
self.conversationModel = conversationModelTmp
|
||||
loadMediaList()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading
|
||||
|
||||
private func loadMediaList() {
|
||||
operationInProgress = true
|
||||
Log.info("\(Self.TAG) Loading media contents for conversation \(conversationModel.chatRoom.identifier ?? "No ID")")
|
||||
|
||||
totalMediaCount = conversationModel.chatRoom.mediaContentsSize
|
||||
Log.info("\(Self.TAG) Media contents size is [\(totalMediaCount)]")
|
||||
|
||||
let contentsToLoad = min(totalMediaCount, Self.CONTENTS_PER_PAGE)
|
||||
let contents = conversationModel.chatRoom.getMediaContentsRange(begin: 0, end: contentsToLoad)
|
||||
|
||||
Log.info("\(Self.TAG) \(contents.count) media have been fetched")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.mediaList = self.getFileModelsList(from: contents)
|
||||
self.operationInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
func loadMoreData(totalItemsCount: Int) {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
Log.info("\(Self.TAG) Loading more data, current total is \(totalItemsCount), max size is \(self.totalMediaCount)")
|
||||
|
||||
guard totalItemsCount < self.totalMediaCount else { return }
|
||||
|
||||
var upperBound = totalItemsCount + Self.CONTENTS_PER_PAGE
|
||||
if upperBound > self.totalMediaCount {
|
||||
upperBound = self.totalMediaCount
|
||||
}
|
||||
|
||||
let contents = self.conversationModel.chatRoom.getMediaContentsRange(begin: totalItemsCount, end: upperBound)
|
||||
Log.info("\(Self.TAG) \(contents.count) contents loaded, adding them to list")
|
||||
|
||||
let newModels = self.getFileModelsList(from: contents)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.mediaList.append(contentsOf: newModels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mapping Content -> FileModel
|
||||
|
||||
private func getFileModelsList(from contents: [Content]) -> [FileModel] {
|
||||
var list: [FileModel] = []
|
||||
|
||||
for mediaContent in contents {
|
||||
|
||||
if mediaContent.isVoiceRecording { continue }
|
||||
|
||||
let isEncrypted = mediaContent.isFileEncrypted
|
||||
let originalPath = mediaContent.filePath ?? ""
|
||||
|
||||
let path: String
|
||||
if isEncrypted {
|
||||
Log.info("\(Self.TAG) [VFS] Content is encrypted, requesting plain file path for file \(originalPath)")
|
||||
path = mediaContent.exportPlainFile()
|
||||
} else {
|
||||
path = originalPath
|
||||
}
|
||||
|
||||
let name = mediaContent.name ?? ""
|
||||
let size = Int64(mediaContent.size)
|
||||
let timestamp = mediaContent.creationTimestamp
|
||||
|
||||
if !path.isEmpty && !name.isEmpty {
|
||||
let model = FileModel(
|
||||
path: path,
|
||||
fileName: name,
|
||||
fileSize: size,
|
||||
fileCreationTimestamp: Int64(timestamp),
|
||||
isEncrypted: isEncrypted,
|
||||
originalPath: originalPath,
|
||||
isFromEphemeralMessage: conversationModel.chatRoom.ephemeralEnabled
|
||||
)
|
||||
list.append(model)
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
|
@ -163,6 +163,11 @@
|
|||
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */; };
|
||||
D7ADF6002AFE356400212231 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADF5FF2AFE356400212231 /* Avatar.swift */; };
|
||||
D7AEB9472F29128500298546 /* Shared.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = D7AEB9462F29128500298546 /* Shared.xcconfig */; };
|
||||
D7AEB9722F324A5E00298546 /* ConversationMediaListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9712F324A5C00298546 /* ConversationMediaListFragment.swift */; };
|
||||
D7AEB9742F324A6F00298546 /* ConversationDocumentsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9732F324A6E00298546 /* ConversationDocumentsListFragment.swift */; };
|
||||
D7AEB9762F39E2A400298546 /* ConversationMediaListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9752F39E2A300298546 /* ConversationMediaListViewModel.swift */; };
|
||||
D7AEB9782F39E2C300298546 /* ConversationDocumentsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9772F39E2C100298546 /* ConversationDocumentsListViewModel.swift */; };
|
||||
D7AEB97A2F39E83600298546 /* FileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AEB9792F39E83500298546 /* FileModel.swift */; };
|
||||
D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; };
|
||||
D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5678D2B28888F00DE63EB /* CallView.swift */; };
|
||||
D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */; };
|
||||
|
|
@ -423,6 +428,11 @@
|
|||
D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
|
||||
D7AEB9462F29128500298546 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
|
||||
D7AEB9712F324A5C00298546 /* ConversationMediaListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMediaListFragment.swift; sourceTree = "<group>"; };
|
||||
D7AEB9732F324A6E00298546 /* ConversationDocumentsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDocumentsListFragment.swift; sourceTree = "<group>"; };
|
||||
D7AEB9752F39E2A300298546 /* ConversationMediaListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMediaListViewModel.swift; sourceTree = "<group>"; };
|
||||
D7AEB9772F39E2C100298546 /* ConversationDocumentsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDocumentsListViewModel.swift; sourceTree = "<group>"; };
|
||||
D7AEB9792F39E83500298546 /* FileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileModel.swift; sourceTree = "<group>"; };
|
||||
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = "<group>"; };
|
||||
D7B5678D2B28888F00DE63EB /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
|
||||
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -638,6 +648,7 @@
|
|||
D70959EF2B8DF33B0014AC0B /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7AEB9792F39E83500298546 /* FileModel.swift */,
|
||||
D77A080D2CB6BCA10095D589 /* MessageConferenceInfo.swift */,
|
||||
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */,
|
||||
D7E6ADF22B9875C20009A2BC /* Message.swift */,
|
||||
|
|
@ -1068,6 +1079,8 @@
|
|||
D7CEE0362B7A212C00FD79B7 /* ViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7AEB9772F39E2C100298546 /* ConversationDocumentsListViewModel.swift */,
|
||||
D7AEB9752F39E2A300298546 /* ConversationMediaListViewModel.swift */,
|
||||
D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */,
|
||||
D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */,
|
||||
D759CB652C3FBE1D00AC35E8 /* StartConversationViewModel.swift */,
|
||||
|
|
@ -1079,6 +1092,8 @@
|
|||
D7CEE0392B7A232200FD79B7 /* Fragments */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7AEB9732F324A6E00298546 /* ConversationDocumentsListFragment.swift */,
|
||||
D7AEB9712F324A5C00298546 /* ConversationMediaListFragment.swift */,
|
||||
D7EFD1E32CD11F53005E67CD /* EphemeralFragment.swift */,
|
||||
D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */,
|
||||
D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */,
|
||||
|
|
@ -1453,6 +1468,7 @@
|
|||
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */,
|
||||
662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */,
|
||||
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
|
||||
D7AEB9742F324A6F00298546 /* ConversationDocumentsListFragment.swift in Sources */,
|
||||
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */,
|
||||
D7C2DA1D2CA44DE400A2441B /* EventModel.swift in Sources */,
|
||||
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
|
||||
|
|
@ -1464,6 +1480,7 @@
|
|||
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */,
|
||||
D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */,
|
||||
D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */,
|
||||
D7AEB9762F39E2A400298546 /* ConversationMediaListViewModel.swift in Sources */,
|
||||
C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */,
|
||||
D7343FEF2D3FE16C0059D784 /* HelpViewModel.swift in Sources */,
|
||||
D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */,
|
||||
|
|
@ -1474,6 +1491,7 @@
|
|||
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */,
|
||||
D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */,
|
||||
D78607712D36CB8A009E6A7E /* SettingsAdvancedFragment.swift in Sources */,
|
||||
D7AEB97A2F39E83600298546 /* FileModel.swift in Sources */,
|
||||
D762102C2E97FDFD002E7999 /* CardDavAddressBookConfigurationFragment.swift in Sources */,
|
||||
66E50A492BD12B2300AD61CA /* MeetingsView.swift in Sources */,
|
||||
D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */,
|
||||
|
|
@ -1482,6 +1500,7 @@
|
|||
C6A5A9452C10B6270070FEA4 /* OIDAuthStateExtension.swift in Sources */,
|
||||
D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */,
|
||||
D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */,
|
||||
D7AEB9722F324A5E00298546 /* ConversationMediaListFragment.swift in Sources */,
|
||||
D7D1F5262EDD91B30034EEB0 /* RecordingMediaPlayerFragment.swift in Sources */,
|
||||
D7C500422D2BE98100DD53EC /* AccountSettingsViewModel.swift in Sources */,
|
||||
D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */,
|
||||
|
|
@ -1576,6 +1595,7 @@
|
|||
D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */,
|
||||
D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */,
|
||||
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */,
|
||||
D7AEB9782F39E2C300298546 /* ConversationDocumentsListViewModel.swift in Sources */,
|
||||
D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */,
|
||||
D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */,
|
||||
D74DA0122C047F0700A8561D /* HistoryModel.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue