From b462657a77d8027c45cde3f4572eae927df9dcaa Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 13 Nov 2025 16:49:33 +0100 Subject: [PATCH] Add recording list --- .../Assets.xcassets/phone.imageset/phone.svg | 2 +- Linphone/Core/CoreContext.swift | 20 ++ Linphone/TelecomManager/TelecomManager.swift | 20 +- Linphone/UI/Main/ContentView.swift | 10 + Linphone/UI/Main/Fragments/SideMenu.swift | 15 +- .../UI/Main/Help/Fragments/HelpFragment.swift | 4 - .../Fragments/HistoryListFragment.swift | 2 + .../Fragments/RecordingsListFragment.swift | 142 ++++++++++++++ .../Recordings/Models/RecordingModel.swift | 180 ++++++++++++++++++ .../ViewModel/RecordingsListViewModel.swift | 92 +++++++++ Linphone/Utils/LinphoneUtils.swift | 5 + LinphoneApp.xcodeproj/project.pbxproj | 44 +++++ 12 files changed, 513 insertions(+), 23 deletions(-) create mode 100644 Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift create mode 100644 Linphone/UI/Main/Recordings/Models/RecordingModel.swift create mode 100644 Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift diff --git a/Linphone/Assets.xcassets/phone.imageset/phone.svg b/Linphone/Assets.xcassets/phone.imageset/phone.svg index ac3ff5a1c..6eb862926 100644 --- a/Linphone/Assets.xcassets/phone.imageset/phone.svg +++ b/Linphone/Assets.xcassets/phone.imageset/phone.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index c2cd38132..023e4680b 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -200,6 +200,26 @@ class CoreContext: ObservableObject { self.forceRemotePushToMatchVoipPushSettings(account: acc) } + let container = FileUtil.sharedContainerUrl() + let recordingsDir = container.appendingPathComponent("Library/Recordings") + + let fm = FileManager.default + + if !fm.fileExists(atPath: recordingsDir.path) { + do { + try fm.createDirectory( + at: recordingsDir, + withIntermediateDirectories: true, + attributes: nil + ) + print("Recordings directory created.") + } catch { + print("Error creating directory: \(error)") + } + } else { + print("Recordings directory already exists.") + } + self.mCoreDelegate = CoreDelegateStub(onGlobalStateChanged: { (core: Core, state: GlobalState, _: String) in if state == GlobalState.On { #if DEBUG diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 3c44bb469..eedf117d2 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -195,17 +195,13 @@ class TelecomManager: ObservableObject { } } - private func makeRecordFilePath() -> String { - var filePath = "recording_" - let now = Date() - let dateFormat = DateFormatter() - dateFormat.dateFormat = "E-d-MMM-yyyy-HH-mm-ss" - let date = dateFormat.string(from: now) - filePath = filePath.appending("\(date).mkv") + private func makeRecordFilePath(address: String) -> String { + var filePath = "call_recording_sip_" + address.dropFirst(4) + "_on_" - let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) - let writablePath = paths[0] - return writablePath.appending("/\(filePath)") + filePath = filePath.appending("\(Int(Date().timeIntervalSince1970)).mkv") + + let writablePath = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Recordings/\(filePath)") + return writablePath.path } func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws { @@ -237,7 +233,7 @@ class TelecomManager: ObservableObject { // Log.directLog(BCTBX_LOG_DEBUG, text: "record file path: \(writablePath)") // lcallParams.recordFile = writablePath - lcallParams.recordFile = makeRecordFilePath() + lcallParams.recordFile = makeRecordFilePath(address: addr.asStringUriOnly()) if isSas { lcallParams.mediaEncryption = .ZRTP @@ -292,7 +288,7 @@ class TelecomManager: ObservableObject { func acceptCall(core: Core, call: Call, hasVideo: Bool) { do { let callParams = try core.createCallParams(call: call) - callParams.recordFile = makeRecordFilePath() + callParams.recordFile = makeRecordFilePath(address: call.remoteAddress?.asStringUriOnly() ?? "") callParams.videoEnabled = hasVideo /*if (ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference")) { let low_bandwidth = (AppManager.network() == .network_2g) diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 3fba105a3..271716a7e 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -65,6 +65,7 @@ struct ContentView: View { @State var isShowConversationFragment = false @State var isShowAccountProfileFragment = false @State var isShowSettingsFragment = false + @State var isShowRecordingsListFragment = false @State var isShowHelpFragment = false @State var fullscreenVideo = false @@ -1041,6 +1042,7 @@ struct ContentView: View { isShowLoginFragment: $isShowLoginFragment, isShowAccountProfileFragment: $isShowAccountProfileFragment, isShowSettingsFragment: $isShowSettingsFragment, + isShowRecordingsListFragment: $isShowRecordingsListFragment, isShowHelpFragment: $isShowHelpFragment ) .environmentObject(accountProfileViewModel) @@ -1285,6 +1287,14 @@ struct ContentView: View { .transition(.move(edge: .trailing)) } + if isShowRecordingsListFragment { + RecordingsListFragment( + isShowRecordingsListFragment: $isShowRecordingsListFragment + ) + .zIndex(3) + .transition(.move(edge: .trailing)) + } + if isShowHelpFragment { HelpFragment( isShowHelpFragment: $isShowHelpFragment diff --git a/Linphone/UI/Main/Fragments/SideMenu.swift b/Linphone/UI/Main/Fragments/SideMenu.swift index f984aa5b1..ae3abe5d7 100644 --- a/Linphone/UI/Main/Fragments/SideMenu.swift +++ b/Linphone/UI/Main/Fragments/SideMenu.swift @@ -32,6 +32,7 @@ struct SideMenu: View { @Binding var isShowLoginFragment: Bool @Binding var isShowAccountProfileFragment: Bool @Binding var isShowSettingsFragment: Bool + @Binding var isShowRecordingsListFragment: Bool @Binding var isShowHelpFragment: Bool @State private var showHelp = false @@ -137,12 +138,15 @@ struct SideMenu: View { } } - /* SideMenuEntry( iconName: "record-fill", title: "recordings_title" - ) - */ + ).onTapGesture { + self.menuClose() + withAnimation { + isShowRecordingsListFragment = true + } + } SideMenuEntry( iconName: "question", @@ -152,7 +156,6 @@ struct SideMenu: View { withAnimation { isShowHelpFragment = true } - } } .padding(.bottom, safeAreaInsets.bottom + 13) @@ -176,15 +179,15 @@ struct SideMenu: View { #Preview { GeometryReader { geometry in - @State var triggerNavigateToLogin: Bool = false SideMenu( width: geometry.size.width / 5 * 4, isOpen: .constant(true), menuClose: {}, safeAreaInsets: geometry.safeAreaInsets, - isShowLoginFragment: $triggerNavigateToLogin, + isShowLoginFragment: .constant(false), isShowAccountProfileFragment: .constant(false), isShowSettingsFragment: .constant(false), + isShowRecordingsListFragment: .constant(false), isShowHelpFragment: .constant(false) ) .ignoresSafeArea(.all) diff --git a/Linphone/UI/Main/Help/Fragments/HelpFragment.swift b/Linphone/UI/Main/Help/Fragments/HelpFragment.swift index 18c55d618..004bda777 100644 --- a/Linphone/UI/Main/Help/Fragments/HelpFragment.swift +++ b/Linphone/UI/Main/Help/Fragments/HelpFragment.swift @@ -25,10 +25,6 @@ struct HelpFragment: View { @Binding var isShowHelpFragment: Bool - @State var advancedSettingsIsOpen: Bool = false - - @FocusState var isVoicemailUriFocused: Bool - var showAssistant: Bool { (CoreContext.shared.coreIsStarted && CoreContext.shared.accounts.isEmpty) || SharedMainViewModel.shared.displayProfileMode diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index c11f57416..c8e8d4240 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -152,8 +152,10 @@ struct HistoryRow: View { if !historyModel.isConf { Image("phone") + .renderingMode(.template) .resizable() .frame(width: 25, height: 25) + .foregroundStyle(Color.grayMain2c600) .padding(.all, 10) .padding(.trailing, 5) .highPriorityGesture( diff --git a/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift b/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift new file mode 100644 index 000000000..54cb18f7b --- /dev/null +++ b/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift @@ -0,0 +1,142 @@ +/* + * 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 SwiftUI + +struct RecordingsListFragment: View { + + @StateObject private var recordingsListViewModel = RecordingsListViewModel() + + @Binding var isShowRecordingsListFragment: Bool + + var body: some View { + NavigationView { + ZStack { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(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, 4) + .padding(.leading, -10) + .onTapGesture { + withAnimation { + isShowRecordingsListFragment = false + } + } + + Text("recordings_title") + .default_text_style_orange_800(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + .lineLimit(1) + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + VStack(spacing: 20) { + ForEach(Array(recordingsListViewModel.recordings.enumerated()), id: \.offset) { index, recording in + if index == 0 || recording.month != recordingsListViewModel.recordings[index-1].month { + createMonthLine(model: recording) + .frame(maxWidth: .infinity, alignment: .leading) + } + + HStack { + VStack { + HStack { + Image("phone") + .renderingMode(.template) + .resizable() + .frame(width: 25, height: 25) + .foregroundStyle(Color.grayMain2c600) + + Text(recording.displayName) + .default_text_style_700(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + } + + Spacer() + + Text(recording.dateTime) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + } + + VStack { + Image("play-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 30, height: 30) + .padding(.leading, -6) + + Spacer() + + Text(recording.formattedDuration) + .default_text_style(styleSize: 14) + .frame(alignment: .center) + } + .padding(.trailing, 6) + } + .frame(height: 60) + .padding(20) + .background(.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: .gray.opacity(0.4), radius: 4) + } + } + .padding(.all, 20) + } + .frame(maxWidth: .infinity) + } + .background(Color.gray100) + } + .background(Color.gray100) + } + .navigationTitle("") + .navigationBarHidden(true) + } + .navigationViewStyle(StackNavigationViewStyle()) + .navigationTitle("") + .navigationBarHidden(true) + } + + @ViewBuilder + func createMonthLine(model: RecordingModel) -> some View { + Text(model.month) + .fontWeight(.bold) + .padding(5) + .default_text_style_500(styleSize: 22) + } +} diff --git a/Linphone/UI/Main/Recordings/Models/RecordingModel.swift b/Linphone/UI/Main/Recordings/Models/RecordingModel.swift new file mode 100644 index 000000000..a345b7c25 --- /dev/null +++ b/Linphone/UI/Main/Recordings/Models/RecordingModel.swift @@ -0,0 +1,180 @@ +/* + * 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 SwiftUI + +class RecordingModel: ObservableObject { + static let TAG = "[Recording Model]" + + var filePath: String = "" + var fileName: String = "" + var sipUri: String = "" + var displayName: String = "" + var timestamp: Int64 = 0 + var month: String = "" + var dateTime: String = "" + var formattedDuration: String = "" + var duration: Int = 0 + + init(filePath: String, fileName: String, isLegacy: Bool = false) { + self.filePath = filePath + self.fileName = fileName + + var sipUriTmp: String = "" + var displayNameTmp: String = "" + var timestampTmp: Int64 = 0 + + CoreContext.shared.doOnCoreQueue { core in + if isLegacy { + let parts = fileName.split(separator: "_") + let username = String(parts.first ?? "") + let sipAddress = core.interpretUrl(url: username, applyInternationalPrefix: false) + sipUriTmp = sipAddress?.asStringUriOnly() ?? username + + if let address = sipAddress { + ContactsManager.shared.getFriendWithAddressInCoreQueue(address: address) { friendResult in + if let addressFriend = friendResult { + displayNameTmp = addressFriend.name! + } else { + if address.displayName != nil { + displayNameTmp = address.displayName! + } else if address.username != nil { + displayNameTmp = address.username! + } else { + displayNameTmp = String(address.asStringUriOnly().dropFirst(4)) + } + } + } + } else { + displayNameTmp = sipUriTmp + } + + if parts.count > 1 { + let parsedDate = String(parts[1]) + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy-HH-mm-ss" + if let date = formatter.date(from: parsedDate) { + timestampTmp = Int64(date.timeIntervalSince1970 * 1000) + } else { + Log.error("\(RecordingModel.TAG) Failed to parse legacy timestamp \(parsedDate)") + } + } + } else { + let headerLength = LinphoneUtils.RECORDING_FILE_NAME_HEADER.count + let withoutHeader = String(fileName.dropFirst(headerLength)) + guard let sepRange = withoutHeader.range(of: LinphoneUtils.RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR) else { + fatalError("\(RecordingModel.TAG) Invalid file name format \(withoutHeader)") + } + + sipUriTmp = String(withoutHeader[.. String { + let locale = Locale.current + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + + let dateFormatter = DateFormatter() + dateFormatter.locale = locale + + if Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) { + dateFormatter.dateFormat = locale.identifier == "fr_FR" + ? "EEEE d MMMM" + : "EEEE, MMMM d" + } else { + dateFormatter.dateFormat = locale.identifier == "fr_FR" + ? "EEEE d MMMM yyyy" + : "EEEE, MMMM d, yyyy" + } + + let timeFormatter = DateFormatter() + timeFormatter.locale = locale + timeFormatter.dateFormat = locale.identifier == "fr_FR" + ? "HH:mm" + : "h:mm a" + + return "\(dateFormatter.string(from: date).capitalized) - \(timeFormatter.string(from: date))" + } + + func formattedMonthYear(timestamp: Int64) -> String { + let locale = Locale.current + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + + let formatter = DateFormatter() + formatter.locale = locale + if Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) { + formatter.dateFormat = "MMMM" + } else { + formatter.dateFormat = "MMMM yyyy" + } + + return formatter.string(from: date).capitalized + } +} diff --git a/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift b/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift new file mode 100644 index 000000000..ada873838 --- /dev/null +++ b/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift @@ -0,0 +1,92 @@ +/* + * 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 Foundation +import Combine + +class RecordingsListViewModel: ObservableObject { + private let TAG = "[RecordingsListViewModel]" + + @Published var recordings: [RecordingModel] = [] + @Published var searchBarVisible: Bool = false + @Published var searchFilter: String = "" + @Published var fetchInProgress: Bool = true + + @Published var focusSearchBarEvent: Bool? = nil + + private let legacyRecordRegex = try! NSRegularExpression(pattern: ".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*") + + init() { + fetchInProgress = true + CoreContext.shared.doOnCoreQueue { core in + self.computeList(filter: "") + } + } + + func openSearchBar() { + searchBarVisible = true + focusSearchBarEvent = true + } + + func closeSearchBar() { + clearFilter() + searchBarVisible = false + focusSearchBarEvent = false + } + + func clearFilter() { + if searchFilter.isEmpty { + searchBarVisible = false + focusSearchBarEvent = false + } else { + searchFilter = "" + } + } + + func applyFilter(_ filter: String) { + DispatchQueue.global(qos: .background).async { + self.computeList(filter: filter) + } + } + + private func computeList(filter: String) { + var list: [RecordingModel] = [] + + let dir1 = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Recordings") + if let files = try? FileManager.default.contentsOfDirectory(at: dir1, includingPropertiesForKeys: nil) { + for file in files { + let path = file.path + let name = file.lastPathComponent + + let model = RecordingModel(filePath: path, fileName: name) + + if filter.isEmpty || model.sipUri.contains(filter) { + list.append(model) + } + } + } + + list.sort { $0.timestamp > $1.timestamp } + + DispatchQueue.main.async { + self.recordings = list + self.fetchInProgress = false + } + } +} diff --git a/Linphone/Utils/LinphoneUtils.swift b/Linphone/Utils/LinphoneUtils.swift index b5cc01df5..0d6a4e79e 100644 --- a/Linphone/Utils/LinphoneUtils.swift +++ b/Linphone/Utils/LinphoneUtils.swift @@ -21,6 +21,11 @@ import Foundation import linphonesw class LinphoneUtils: NSObject { + static let RECORDING_FILE_NAME_HEADER = "call_recording_sip_" + static let RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR = "_on_" + static let RECORDING_MKV_FILE_EXTENSION = ".mkv" + static let RECORDING_SMFF_FILE_EXTENSION = ".smff" + public class func isChatRoomAGroup(chatRoom: ChatRoom) -> Bool { let oneToOne = chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) let conference = chatRoom.hasCapability(mask: ChatRoom.Capabilities.Conference.rawValue) diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index d958e07c6..2c8e765f1 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -147,6 +147,9 @@ D78E062C2BEA69BC00CE3783 /* CallStatisticsSheetBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062B2BEA69BC00CE3783 /* CallStatisticsSheetBottomSheet.swift */; }; D78E062E2BEA69F400CE3783 /* AudioRouteBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062D2BEA69F400CE3783 /* AudioRouteBottomSheet.swift */; }; D78E06302BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062F2BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift */; }; + D795F57E2EC5F9500022C17D /* RecordingsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */; }; + D795F5802EC5F9660022C17D /* RecordingsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */; }; + D795F5832EC6133C0022C17D /* RecordingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795F5822EC6133A0022C17D /* RecordingModel.swift */; }; D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79622332B1DFE600037EACD /* DialerBottomSheet.swift */; }; D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; }; D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */; }; @@ -379,6 +382,9 @@ D78E062B2BEA69BC00CE3783 /* CallStatisticsSheetBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallStatisticsSheetBottomSheet.swift; sourceTree = ""; }; D78E062D2BEA69F400CE3783 /* AudioRouteBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteBottomSheet.swift; sourceTree = ""; }; D78E062F2BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLayoutBottomSheet.swift; sourceTree = ""; }; + D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsListFragment.swift; sourceTree = ""; }; + D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsListViewModel.swift; sourceTree = ""; }; + D795F5822EC6133A0022C17D /* RecordingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingModel.swift; sourceTree = ""; }; D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = ""; }; D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationInfoFragment.swift; sourceTree = ""; }; @@ -695,6 +701,7 @@ D7A03FBE2ACC2E010081A588 /* History */, 66E56BC52BA45E49006CE56F /* Meetings */, 66D382032CEB7DB80063E1C5 /* Models */, + D795F57A2EC5F89B0022C17D /* Recordings */, D7DC096A2CFA192200A6D47C /* Settings */, D7A2EDD42AC180FE005D90FC /* Viewmodel */, D719ABB82ABC67BF00B41C10 /* ContentView.swift */, @@ -895,6 +902,40 @@ path = ViewModel; sourceTree = ""; }; + D795F57A2EC5F89B0022C17D /* Recordings */ = { + isa = PBXGroup; + children = ( + D795F57B2EC5F8FF0022C17D /* Fragments */, + D795F5812EC613220022C17D /* Models */, + D795F57C2EC5F9090022C17D /* ViewModel */, + ); + path = Recordings; + sourceTree = ""; + }; + D795F57B2EC5F8FF0022C17D /* Fragments */ = { + isa = PBXGroup; + children = ( + D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */, + ); + path = Fragments; + sourceTree = ""; + }; + D795F57C2EC5F9090022C17D /* ViewModel */ = { + isa = PBXGroup; + children = ( + D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + D795F5812EC613220022C17D /* Models */ = { + isa = PBXGroup; + children = ( + D795F5822EC6133A0022C17D /* RecordingModel.swift */, + ); + path = Models; + sourceTree = ""; + }; D7A03FBB2ACC2D850081A588 /* Contacts */ = { isa = PBXGroup; children = ( @@ -1280,6 +1321,7 @@ buildActionMask = 2147483647; files = ( D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */, + D795F57E2EC5F9500022C17D /* RecordingsListFragment.swift in Sources */, D7ADF6002AFE356400212231 /* Avatar.swift in Sources */, D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */, D71707202AC5989C0037746F /* TextExtension.swift in Sources */, @@ -1380,8 +1422,10 @@ D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */, D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */, 6613A0B42BAEBE3F008923A4 /* MeetingViewModel.swift in Sources */, + D795F5832EC6133C0022C17D /* RecordingModel.swift in Sources */, D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */, 66C492012B24DB6900CEA16D /* Log.swift in Sources */, + D795F5802EC5F9660022C17D /* RecordingsListViewModel.swift in Sources */, D756C8182D352C5F00A58F2F /* CorePreferences.swift in Sources */, C6A5A9432C10B5ED0070FEA4 /* DecodableExtension.swift in Sources */, D714035B2BE11E00004BD8CA /* CallMediaEncryptionModel.swift in Sources */,