From fe8432f128aba48c1adbb5c2a9c7db8d87fc4368 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 4 Dec 2025 16:33:15 +0100 Subject: [PATCH] Add a bottom sheet in RecordingsListFragment and display an empty state when the list is empty --- Linphone/GeneratedGit.swift | 2 +- .../Localizable/en.lproj/Localizable.strings | 6 + .../Localizable/fr.lproj/Localizable.strings | 3 + .../UI/Call/MeetingWaitingRoomFragment.swift | 2 +- .../Contacts/Fragments/ContactsFragment.swift | 2 +- .../History/Fragments/HistoryFragment.swift | 2 +- .../Fragments/RecordingsListBottomSheet.swift | 176 ++++++++++++ .../Fragments/RecordingsListFragment.swift | 251 +++++++++++------- .../Recordings/Models/RecordingModel.swift | 4 +- .../ViewModel/RecordingsListViewModel.swift | 2 + LinphoneApp.xcodeproj/project.pbxproj | 4 + 11 files changed, 351 insertions(+), 103 deletions(-) create mode 100644 Linphone/UI/Main/Recordings/Fragments/RecordingsListBottomSheet.swift diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift index e6941c350..6ee18628d 100644 --- a/Linphone/GeneratedGit.swift +++ b/Linphone/GeneratedGit.swift @@ -1,5 +1,5 @@ import Foundation public let APP_GIT_BRANCH = "master" -public let APP_GIT_COMMIT = "5492a3e3a" +public let APP_GIT_COMMIT = "36fa752cc" public let APP_GIT_TAG = "6.1.0-alpha" diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index cf1b49e09..dabf54ca1 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -406,6 +406,8 @@ "menu_resend_chat_message" = "Re-send"; "menu_see_existing_contact" = "See contact"; "menu_show_imdn" = "Delivery status"; +"menu_export_selected_item" = "Download"; +"menu_share_selected_item" = "Share"; "message_copied_to_clipboard_toast" = "Message copied into clipboard"; "message_delivery_info_error_title" = "Error"; "message_delivery_info_read_title" = "Read"; @@ -435,6 +437,10 @@ "picker_categories" = "Categories"; "qr_code_validated" = "QR code validated"; "recordings_title" = "Recordings"; +"recordings_list_empty" = "No recording for the moment…"; +"recordings_list_empty" = "No recording for the moment…"; +"recordings_list_empty" = "No recording for the moment…"; +"recordings_list_empty" = "No recording for the moment…"; "selected_participants_count" = "%@ selected participants"; "settings_advanced_accept_early_media_title" = "Accept early media"; "settings_advanced_allow_outgoing_early_media_title" = "Allow outgoing early media"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index d9b064b5d..ab40a6d2a 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -406,6 +406,8 @@ "menu_resend_chat_message" = "Ré-envoyer"; "menu_see_existing_contact" = "Voir le contact"; "menu_show_imdn" = "Info de réception"; +"menu_export_selected_item" = "Télécharger"; +"menu_share_selected_item" = "Partager"; "message_copied_to_clipboard_toast" = "Message copié dans le presse-papier"; "message_delivery_info_error_title" = "En erreur"; "message_delivery_info_read_title" = "Lu"; @@ -435,6 +437,7 @@ "picker_categories" = "Catégories"; "qr_code_validated" = "QR code validé"; "recordings_title" = "Enregistrements"; +"recordings_list_empty" = "Aucun appel enregistré…"; "selected_participants_count" = "%@ participants selectionnés"; "settings_advanced_accept_early_media_title" = "Accepter l'early media"; "settings_advanced_allow_outgoing_early_media_title" = "Autoriser l'early media pour les appels sortants"; diff --git a/Linphone/UI/Call/MeetingWaitingRoomFragment.swift b/Linphone/UI/Call/MeetingWaitingRoomFragment.swift index a8cd1482a..485f956f6 100644 --- a/Linphone/UI/Call/MeetingWaitingRoomFragment.swift +++ b/Linphone/UI/Call/MeetingWaitingRoomFragment.swift @@ -48,7 +48,7 @@ struct MeetingWaitingRoomFragment: View { .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false }, content: { - innerBottomSheet().presentationDetents([.fraction(0.3)]) + innerBottomSheet().presentationDetents([.fraction(0.4)]) }) .onAppear { meetingWaitingRoomViewModel.enableAVAudioSession() diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift index a12e50a13..c38e321ac 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift @@ -41,7 +41,7 @@ struct ContactsFragment: View { showingSheet: $showingSheet, showShareSheet: $showShareSheet ) - .presentationDetents(contactsListViewModel.selectedFriend?.isReadOnly == true ? [.fraction(0.1)] : [.fraction(0.3)]) + .presentationDetents(contactsListViewModel.selectedFriend?.isReadOnly == true ? [.fraction(0.1)] : [.fraction(0.4)]) } .sheet(isPresented: $showShareSheet) { ShareSheet(friendToShare: contactsListViewModel.selectedFriendToShare!) diff --git a/Linphone/UI/Main/History/Fragments/HistoryFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryFragment.swift index 86b640e1b..e50b99a84 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryFragment.swift @@ -39,7 +39,7 @@ struct HistoryFragment: View { isShowEditContactFragment: $isShowEditContactFragment, isShowEditContactFragmentAddress: $isShowEditContactFragmentAddress ) - .presentationDetents([.fraction(0.3)]) + .presentationDetents([.fraction(0.4)]) } } else { HistoryListFragment(showingSheet: $showingSheet, text: $text) diff --git a/Linphone/UI/Main/Recordings/Fragments/RecordingsListBottomSheet.swift b/Linphone/UI/Main/Recordings/Fragments/RecordingsListBottomSheet.swift new file mode 100644 index 000000000..c5d11d81b --- /dev/null +++ b/Linphone/UI/Main/Recordings/Fragments/RecordingsListBottomSheet.swift @@ -0,0 +1,176 @@ +/* + * 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 +import linphonesw + +struct RecordingsListBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @State private var orientation = UIDevice.current.orientation + + @EnvironmentObject var recordingsListViewModel: RecordingsListViewModel + + @Binding var showingSheet: Bool + @Binding var showShareSheet: Bool + @Binding var showPicker: Bool + + var body: some View { + VStack(alignment: .leading) { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("dialog_close") { + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } + } + .padding(.trailing) + } + + Spacer() + + if let selectedRecording = recordingsListViewModel.selectedRecording { + Button { + showShareSheet = true + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("share-network") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text("menu_share_selected_item") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + showPicker = true + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("download-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text("menu_export_selected_item") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + recordingsListViewModel.recordings.removeAll { $0.filePath == selectedRecording.filePath } + selectedRecording.delete() + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("trash") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + Text("menu_delete_selected_item") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + } + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + .onRotate { newOrientation in + orientation = newOrientation + } + } +} + +#Preview { + ConversationsListBottomSheet(showingSheet: .constant(true)) +} diff --git a/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift b/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift index 7b4920531..31e96008d 100644 --- a/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift +++ b/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift @@ -23,111 +23,44 @@ struct RecordingsListFragment: View { @StateObject private var recordingsListViewModel = RecordingsListViewModel() + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Binding var isShowRecordingsListFragment: Bool + @State var showingSheet: Bool = false + @State private var showShareSheet: Bool = false + @State private var showPicker: Bool = false + 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) - } - - NavigationLink(destination: LazyView { - RecordingMediaPlayerFragment(recording: recording) - }) { - 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) + if #available(iOS 16.4, *), idiom != .pad { + innerView() + .sheet(isPresented: $showingSheet) { + RecordingsListBottomSheet(showingSheet: $showingSheet, showShareSheet: $showShareSheet, showPicker: $showPicker) + .environmentObject(recordingsListViewModel) + .presentationDetents([.fraction(0.4)]) } - .frame(maxWidth: .infinity) - } - .background(Color.gray100) + } else { + innerView() + .halfSheet(showSheet: $showingSheet) { + RecordingsListBottomSheet(showingSheet: $showingSheet, showShareSheet: $showShareSheet, showPicker: $showPicker) + .environmentObject(recordingsListViewModel) + } onDismiss: {} } - .background(Color.gray100) } + .sheet(isPresented: $showShareSheet) { + if let selectedRecording = recordingsListViewModel.selectedRecording, let url = URL(string: "file://" + selectedRecording.filePath) { + ShareAnySheet(items: [url]) + .edgesIgnoringSafeArea(.bottom) + } + } + .sheet(isPresented: $showPicker) { + if let selectedRecording = recordingsListViewModel.selectedRecording, let url = URL(string: "file://" + selectedRecording.filePath) { + DocumentSaver(fileURL: url) + .edgesIgnoringSafeArea(.bottom) + } + } .navigationTitle("") .navigationBarHidden(true) } @@ -136,6 +69,130 @@ struct RecordingsListFragment: View { .navigationBarHidden(true) } + @ViewBuilder + func innerView() -> some View { + 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) + } + + NavigationLink(destination: LazyView { + RecordingMediaPlayerFragment(recording: recording) + }) { + 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) + .onLongPressGesture(minimumDuration: 0.3) { + touchFeedback() + recordingsListViewModel.selectedRecording = recording + showingSheet = true + } + } + } + } + .padding(.all, 20) + } + .frame(maxWidth: .infinity) + } + .overlay( + VStack { + if recordingsListViewModel.recordings.count == 0 { + Spacer() + Image("illus-belledonne") + .resizable() + .scaledToFit() + .clipped() + .padding() + Text("recordings_list_empty") + .default_text_style_800(styleSize: 16) + Spacer() + Spacer() + } + } + ) + .background(Color.gray100) + } + .background(Color.gray100) + } + @ViewBuilder func createMonthLine(model: RecordingModel) -> some View { Text(model.month) diff --git a/Linphone/UI/Main/Recordings/Models/RecordingModel.swift b/Linphone/UI/Main/Recordings/Models/RecordingModel.swift index a345b7c25..aee2eac39 100644 --- a/Linphone/UI/Main/Recordings/Models/RecordingModel.swift +++ b/Linphone/UI/Main/Recordings/Models/RecordingModel.swift @@ -132,9 +132,9 @@ class RecordingModel: ObservableObject { } } - func delete() async { + func delete() { Log.info("\(RecordingModel.TAG) Deleting call recording \(filePath)") - //await FileUtils.deleteFile(path: filePath) + FileUtil.delete(path: filePath) } func formattedDateTime(timestamp: Int64) -> String { diff --git a/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift b/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift index ada873838..d04dee292 100644 --- a/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift +++ b/Linphone/UI/Main/Recordings/ViewModel/RecordingsListViewModel.swift @@ -29,6 +29,8 @@ class RecordingsListViewModel: ObservableObject { @Published var fetchInProgress: Bool = true @Published var focusSearchBarEvent: Bool? = nil + + var selectedRecording: RecordingModel? private let legacyRecordRegex = try! NSRegularExpression(pattern: ".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*") diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index 210f2605a..c74722af0 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; }; D737AEEF2DA011F2005C1280 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D737AEED2DA011F2005C1280 /* Localizable.strings */; }; D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */; }; + D73DD7D02EE184B200654313 /* RecordingsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73DD7CF2EE184A500654313 /* RecordingsListBottomSheet.swift */; }; D7458F392E0BDCF4000C957A /* linphoneExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; }; D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; }; @@ -355,6 +356,7 @@ D737AEEE2DA011F2005C1280 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable/en.lproj/Localizable.strings; sourceTree = ""; }; D737AEF02DA01203005C1280 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable/fr.lproj/Localizable.strings; sourceTree = ""; }; D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardResponder.swift; sourceTree = ""; }; + D73DD7CF2EE184A500654313 /* RecordingsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsListBottomSheet.swift; sourceTree = ""; }; D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = linphoneExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; @@ -922,6 +924,7 @@ D795F57B2EC5F8FF0022C17D /* Fragments */ = { isa = PBXGroup; children = ( + D73DD7CF2EE184A500654313 /* RecordingsListBottomSheet.swift */, D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */, D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */, ); @@ -1434,6 +1437,7 @@ 66162A202BDFC2F900DCE913 /* AddParticipantsViewModel.swift in Sources */, D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */, D78E06302BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift in Sources */, + D73DD7D02EE184B200654313 /* RecordingsListBottomSheet.swift in Sources */, 66E56BC92BA4A6D7006CE56F /* MeetingsListViewModel.swift in Sources */, D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */, C67586AE2C09F23C002E77BF /* URLExtension.swift in Sources */,