From 4f1fcbbcf63272c33ee968098a5f4e9b023b0e22 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 6 May 2025 16:39:17 +0200 Subject: [PATCH] Added file picker, updated unsecured chatroom icon, and replaced floating button with a scroll-down icon in the chatroom --- Linphone.xcodeproj/project.pbxproj | 8 +- .../camera.imageset/camera.svg | 2 +- .../caret-double-down.imageset/Contents.json | 21 ++ .../caret-double-down.svg | 1 + .../image.imageset/Contents.json | 21 ++ .../Assets.xcassets/image.imageset/image.svg | 1 + .../Contents.json | 21 ++ .../lock-simple-open-bold.svg | 1 + .../lock-simple-open.imageset/Contents.json | 21 ++ .../lock-simple-open.svg | 1 + Linphone/Core/CoreContext.swift | 3 +- .../Fragments/ChatBubbleView.swift | 65 ++++-- .../Fragments/ConversationFragment.swift | 177 +++++++++++----- .../Fragments/ConversationsListFragment.swift | 16 +- .../Main/Conversations/Fragments/UIList.swift | 15 +- .../Model/ConversationModel.swift | 33 +++ .../ViewModel/ConversationViewModel.swift | 195 ++++++++---------- .../ConversationsListViewModel.swift | 3 + Linphone/Utils/FilePicker.swift | 111 ++++++++++ Linphone/Utils/FileUtils.swift | 21 ++ Linphone/Utils/PhotoPicker.swift | 3 +- Linphone/en.lproj/Localizable.strings | 4 + Linphone/fr.lproj/Localizable.strings | 4 + 23 files changed, 568 insertions(+), 180 deletions(-) create mode 100644 Linphone/Assets.xcassets/caret-double-down.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/caret-double-down.imageset/caret-double-down.svg create mode 100644 Linphone/Assets.xcassets/image.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/image.imageset/image.svg create mode 100644 Linphone/Assets.xcassets/lock-simple-open-bold.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/lock-simple-open-bold.imageset/lock-simple-open-bold.svg create mode 100644 Linphone/Assets.xcassets/lock-simple-open.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/lock-simple-open.imageset/lock-simple-open.svg create mode 100644 Linphone/Utils/FilePicker.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index eea1ff2b7..6b4c665ad 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 4ED1F0A881A9ACB5977A8987 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + 4ED1F0A881A9ACB5977A8987 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; 660AAF7F2B839272004C0FA6 /* msgNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 660D8A712B517D260092694D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 660D8A702B517D260092694D /* GoogleService-Info.plist */; }; 6613A0AE2BAEB7DF008923A4 /* MeetingFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0AD2BAEB7DF008923A4 /* MeetingFragment.swift */; }; @@ -56,6 +56,7 @@ C6A5A9482C10B6A30070FEA4 /* AuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A5A9472C10B6A30070FEA4 /* AuthState.swift */; }; C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DC4E3C2C199C4E009096FD /* BundleExtenion.swift */; }; C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */; }; + D703F7082DC8C605005B8F75 /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D703F7072DC8C5FF005B8F75 /* FilePicker.swift */; }; D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */; }; D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */; }; D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */; }; @@ -262,6 +263,7 @@ C6A5A9472C10B6A30070FEA4 /* AuthState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthState.swift; sourceTree = ""; }; C6DC4E3C2C199C4E009096FD /* BundleExtenion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtenion.swift; sourceTree = ""; }; C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuEntry.swift; sourceTree = ""; }; + D703F7072DC8C5FF005B8F75 /* FilePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePicker.swift; sourceTree = ""; }; D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = ""; }; D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListBottomSheet.swift; sourceTree = ""; }; @@ -413,7 +415,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4ED1F0A881A9ACB5977A8987 /* (null) in Frameworks */, + 4ED1F0A881A9ACB5977A8987 /* BuildFile in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -542,6 +544,7 @@ D717071C2AC591EF0037746F /* Utils */ = { isa = PBXGroup; children = ( + D703F7072DC8C5FF005B8F75 /* FilePicker.swift */, D717A10D2CEB770D00849D92 /* ShareSheetController.swift */, 66C491F72B24D25A00CEA16D /* Extensions */, 66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */, @@ -1346,6 +1349,7 @@ D74DA0122C047F0700A8561D /* HistoryModel.swift in Sources */, 66D382052CEB7E0A0063E1C5 /* ShortcutModel.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, + D703F7082DC8C605005B8F75 /* FilePicker.swift in Sources */, C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */, D714DE622C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift in Sources */, 66C468FB2D2BE54800A836F7 /* PIPViewModel.swift in Sources */, diff --git a/Linphone/Assets.xcassets/camera.imageset/camera.svg b/Linphone/Assets.xcassets/camera.imageset/camera.svg index 7a8d0ac40..cf6dbbf65 100644 --- a/Linphone/Assets.xcassets/camera.imageset/camera.svg +++ b/Linphone/Assets.xcassets/camera.imageset/camera.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/caret-double-down.imageset/Contents.json b/Linphone/Assets.xcassets/caret-double-down.imageset/Contents.json new file mode 100644 index 000000000..f9cf36f84 --- /dev/null +++ b/Linphone/Assets.xcassets/caret-double-down.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "caret-double-down.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/caret-double-down.imageset/caret-double-down.svg b/Linphone/Assets.xcassets/caret-double-down.imageset/caret-double-down.svg new file mode 100644 index 000000000..f6f5f2556 --- /dev/null +++ b/Linphone/Assets.xcassets/caret-double-down.imageset/caret-double-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/image.imageset/Contents.json b/Linphone/Assets.xcassets/image.imageset/Contents.json new file mode 100644 index 000000000..8f75fcb44 --- /dev/null +++ b/Linphone/Assets.xcassets/image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "image.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/image.imageset/image.svg b/Linphone/Assets.xcassets/image.imageset/image.svg new file mode 100644 index 000000000..25a7a7651 --- /dev/null +++ b/Linphone/Assets.xcassets/image.imageset/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/lock-simple-open-bold.imageset/Contents.json b/Linphone/Assets.xcassets/lock-simple-open-bold.imageset/Contents.json new file mode 100644 index 000000000..4cb1bbebd --- /dev/null +++ b/Linphone/Assets.xcassets/lock-simple-open-bold.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "lock-simple-open-bold.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/lock-simple-open-bold.imageset/lock-simple-open-bold.svg b/Linphone/Assets.xcassets/lock-simple-open-bold.imageset/lock-simple-open-bold.svg new file mode 100644 index 000000000..60cbc520e --- /dev/null +++ b/Linphone/Assets.xcassets/lock-simple-open-bold.imageset/lock-simple-open-bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/lock-simple-open.imageset/Contents.json b/Linphone/Assets.xcassets/lock-simple-open.imageset/Contents.json new file mode 100644 index 000000000..e785d1668 --- /dev/null +++ b/Linphone/Assets.xcassets/lock-simple-open.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "lock-simple-open.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/lock-simple-open.imageset/lock-simple-open.svg b/Linphone/Assets.xcassets/lock-simple-open.imageset/lock-simple-open.svg new file mode 100644 index 000000000..203a421a1 --- /dev/null +++ b/Linphone/Assets.xcassets/lock-simple-open.imageset/lock-simple-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 8f099428e..3807e258f 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -46,6 +46,7 @@ final class CoreContext: ObservableObject { var mCore: Core! var bearerAuthInfoPendingPasswordUpdate: AuthInfo? + var imdnToEverybodyThreshold: Bool = true let monitor = NWPathMonitor() var networkStatusIsConnected: Bool = true // updated on core queue @@ -154,7 +155,7 @@ final class CoreContext: ObservableObject { self.mCore.config!.setString(section: "misc", key: "version_check_url_root", value: "https://download.linphone.org/releases") self.mCore.imdnToEverybodyThreshold = 1 - + self.imdnToEverybodyThreshold = self.mCore.imdnToEverybodyThreshold == 1 //self.copyDatabaseFileToDocumentsDirectory() let shortcutsCount = self.mCore.config!.getInt(section: "ui", key: "shortcut_count", defaultValue: 0) diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 717aefad6..f6d75e4af 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -340,7 +340,7 @@ struct ChatBubbleView: View { .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) .frame(width: 10, height: 10) .padding(.top, 1) - } else if eventLogMessage.message.status != nil { + } else if eventLogMessage.message.status != nil && !(CoreContext.shared.imdnToEverybodyThreshold && !eventLogMessage.message.isOutgoing) { Image(conversationViewModel.getImageIMDN(status: eventLogMessage.message.status!)) .renderingMode(.template) .resizable() @@ -374,8 +374,10 @@ struct ChatBubbleView: View { } } .onTapGesture { - conversationViewModel.selectedMessageToDisplayDetails = eventLogMessage - conversationViewModel.prepareBottomSheetForDeliveryStatus() + if !(CoreContext.shared.imdnToEverybodyThreshold && !eventLogMessage.message.isOutgoing) { + conversationViewModel.selectedMessageToDisplayDetails = eventLogMessage + conversationViewModel.prepareBottomSheetForDeliveryStatus() + } } .disabled(conversationViewModel.selectedMessage != nil) .padding(.top, -4) @@ -542,7 +544,6 @@ struct ChatBubbleView: View { onImageTapped: { selectedURLAttachment = eventLogMessage.message.attachments.first!.full }) - .overlay( Group { if eventLogMessage.message.attachments.first!.type == .video { @@ -645,10 +646,24 @@ struct ChatBubbleView: View { .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) - Text(eventLogMessage.message.attachments.first!.size.formatBytes()) - .default_text_style_300(styleSize: 14) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) + if eventLogMessage.message.attachments.first!.size > 0 { + Text(eventLogMessage.message.attachments.first!.size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + if let size = self.getFileSize(atPath: eventLogMessage.message.attachments.first!.full.path) { + Text(size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Text(eventLogMessage.message.attachments.first!.size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } } .padding(.horizontal, 10) .frame(maxWidth: .infinity, alignment: .leading) @@ -762,10 +777,24 @@ struct ChatBubbleView: View { .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) - Text(attachment.size.formatBytes()) - .default_text_style_300(styleSize: 14) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) + if attachment.size > 0 { + Text(attachment.size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + if let size = self.getFileSize(atPath: attachment.full.path) { + Text(size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Text(attachment.size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } } .padding(.horizontal, 10) .frame(maxWidth: .infinity, alignment: .leading) @@ -830,6 +859,18 @@ struct ChatBubbleView: View { } } } + + private func getFileSize(atPath path: String) -> Int? { + do { + let attributes = try FileManager.default.attributesOfItem(atPath: path) + if let fileSize = attributes[.size] as? Int { + return fileSize + } + } catch { + print("Error: \(error)") + } + return nil + } } struct DynamicLinkText: View { diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index c0af89ce9..37ce019eb 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -55,8 +55,11 @@ struct ConversationFragment: View { @State private var displayFloatingButton = false + @State private var areFilePickersOpen = false + @State private var isShowPhotoLibrary = false @State private var isShowCamera = false + @State private var isShowFilePicker = false @State private var mediasIsLoading = false @State private var voiceRecordingInProgress = false @@ -117,6 +120,23 @@ struct ConversationFragment: View { } .edgesIgnoringSafeArea(.all) }) + .sheet(isPresented: $isShowFilePicker, onDismiss: { + isShowFilePicker = false + }, content: { + FilePicker(onDocumentsPicked: { urlList in + FilePicker.convertToAttachmentArray(fromResults: urlList) { mediasOrNil, errorOrNil in + if let error = errorOrNil { + print(error) + } + + if let medias = mediasOrNil { + conversationViewModel.mediasToSend.append(contentsOf: medias) + } + self.mediasIsLoading = false + } + }) + .edgesIgnoringSafeArea(.all) + }) .fullScreenCover(isPresented: $isShowCamera) { ImagePicker(conversationViewModel: conversationViewModel, selectedMedia: self.$conversationViewModel.mediasToSend) .edgesIgnoringSafeArea(.all) @@ -421,7 +441,7 @@ struct ConversationFragment: View { } label: { ZStack { - Image("caret-down") + Image("caret-double-down") .renderingMode(.template) .foregroundStyle(.white) .padding() @@ -555,23 +575,40 @@ struct ConversationFragment: View { .fill(Color(.white)) .frame(width: 100, height: 100) - AsyncImage(url: attachment.thumbnail) { image in - ZStack { - image - .resizable() - .interpolation(.medium) - .aspectRatio(contentMode: .fill) - - if attachment.type == .video { - Image("play-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 40, height: 40, alignment: .leading) + VStack { + if attachment.type == .image || attachment.type == .gif || attachment.type == .video { + AsyncImage(url: attachment.thumbnail) { image in + ZStack { + image + .resizable() + .interpolation(.medium) + .aspectRatio(contentMode: .fill) + + if attachment.type == .video { + Image("play-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 40, height: 40, alignment: .leading) + } + } + } placeholder: { + ProgressView() } + } else { + VStack { + Spacer() + Text(attachment.name) + .foregroundStyle(Color.grayMain2c700) + .default_text_style_800(styleSize: 14) + .truncationMode(.middle) + .frame(maxWidth: .infinity, alignment: .center) + .multilineTextAlignment(.center) + .lineLimit(2) + Spacer() + } + .background(Color.grayMain2c200) } - } placeholder: { - ProgressView() } .layoutPriority(-1) .onTapGesture { @@ -620,46 +657,94 @@ struct ConversationFragment: View { .transition(.move(edge: .bottom)) } + if areFilePickersOpen { + ZStack(alignment: .top) { + HStack { + Button { + self.areFilePickersOpen.toggle() + self.isShowCamera = true + } label: { + VStack { + Image("camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + + Text("conversation_take_picture_label") + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .center) + .lineLimit(1) + } + } + .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) + + Button { + self.areFilePickersOpen.toggle() + self.isShowPhotoLibrary = true + self.mediasIsLoading = true + } label: { + VStack { + Image("image") + .renderingMode(.template) + .resizable() + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + + Text("conversation_pick_file_from_gallery_label") + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .center) + .lineLimit(1) + } + } + .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) + + Button { + self.areFilePickersOpen.toggle() + self.isShowFilePicker = true + self.mediasIsLoading = true + } label: { + VStack { + Image("file") + .renderingMode(.template) + .resizable() + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + + Text("conversation_pick_any_file_label") + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .center) + .lineLimit(1) + } + } + .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) + } + .frame(maxWidth: .infinity) + .padding(.all, 20) + .background(Color.gray100) + } + .transition(.move(edge: .bottom)) + } + HStack(spacing: 0) { if !voiceRecordingInProgress { Button { + withAnimation { + areFilePickersOpen.toggle() + } } label: { - Image("smiley") + Image(areFilePickersOpen ? "x" : "paperclip") .renderingMode(.template) .resizable() - .foregroundStyle(Color.grayMain2c500) + .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) .frame(width: 28, height: 28, alignment: .leading) .padding(.all, 6) .padding(.top, 4) - } - .padding(.horizontal, isMessageTextFocused ? 0 : 2) - - Button { - self.isShowPhotoLibrary = true - self.mediasIsLoading = true - } label: { - Image("paperclip") - .renderingMode(.template) - .resizable() - .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) - .frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading) - .padding(.all, isMessageTextFocused ? 0 : 6) - .padding(.top, 4) - .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) - } - .padding(.horizontal, isMessageTextFocused ? 0 : 2) - - Button { - self.isShowCamera = true - } label: { - Image("camera") - .renderingMode(.template) - .resizable() - .foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500) - .frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading) - .padding(.all, isMessageTextFocused ? 0 : 6) - .padding(.top, 4) .disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading) + .animation(.none, value: areFilePickersOpen) } .padding(.horizontal, isMessageTextFocused ? 0 : 2) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift index 1ba0e7095..e8e64a21e 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift @@ -126,14 +126,6 @@ struct ConversationRow: View { Spacer() HStack { - if !conversation.encryptionEnabled { - Image("warning-circle") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.redDanger500) - .frame(width: 18, height: 18, alignment: .trailing) - } - Text(conversationsListViewModel.getCallTime(startDate: conversation.lastUpdateTime)) .foregroundStyle(Color.grayMain2c400) .default_text_style(styleSize: 14) @@ -151,6 +143,14 @@ struct ConversationRow: View { .frame(width: 18, height: 18, alignment: .trailing) } + if !conversation.encryptionEnabled { + Image("lock-simple-open-bold") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeWarning600) + .frame(width: 18, height: 18, alignment: .trailing) + } + if conversation.isMuted { Image("bell-slash") .renderingMode(.template) diff --git a/Linphone/UI/Main/Conversations/Fragments/UIList.swift b/Linphone/UI/Main/Conversations/Fragments/UIList.swift index 8e9579f59..ea99d9286 100644 --- a/Linphone/UI/Main/Conversations/Fragments/UIList.swift +++ b/Linphone/UI/Main/Conversations/Fragments/UIList.swift @@ -49,8 +49,19 @@ class FloatingButton: UIButton { private func setupButton() { // Set the button's appearance - self.setImage(UIImage(named: "caret-down")?.withRenderingMode(.alwaysTemplate), for: .normal) - self.tintColor = .white + if let originalImage = UIImage(named: "caret-double-down")?.withRenderingMode(.alwaysTemplate) { + let newSize = CGSize(width: originalImage.size.width / 1.5, height: originalImage.size.height / 1.5) + + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) + originalImage.draw(in: CGRect(origin: .zero, size: newSize)) + var resizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + resizedImage = resizedImage?.withRenderingMode(.alwaysTemplate) + + self.setImage(resizedImage, for: .normal) + self.tintColor = .white + } self.backgroundColor = UIColor(Color.orangeMain500) self.layer.cornerRadius = 30 self.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor diff --git a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift index 13213877a..4602fc394 100644 --- a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift +++ b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift @@ -28,6 +28,8 @@ class ConversationModel: ObservableObject, Identifiable { private var contactsManager = ContactsManager.shared var chatRoom: ChatRoom + var lastMessage: ChatMessage? + let isDisabledBecauseNotSecured: Bool = false static let TAG = "[Conversation Model]" @@ -51,6 +53,7 @@ class ConversationModel: ObservableObject, Identifiable { @Published var avatarModel: ContactAvatarModel private var conferenceDelegate: ConferenceDelegate? + private var chatMessageDelegate: ChatMessageDelegate? init(chatRoom: ChatRoom) { self.chatRoom = chatRoom @@ -77,6 +80,8 @@ class ConversationModel: ObservableObject, Identifiable { self.encryptionEnabled = chatRoom.currentParams != nil && chatRoom.currentParams!.encryptionEnabled + self.lastMessage = nil + self.lastMessageText = "" self.lastMessageIsOutgoing = false @@ -182,9 +187,37 @@ class ConversationModel: ObservableObject, Identifiable { } } + func chatMessageAddDelegate() { + if self.lastMessage != nil && self.chatMessageDelegate != nil { + self.lastMessage!.removeDelegate(delegate: self.chatMessageDelegate!) + } + + self.chatMessageDelegate = ChatMessageDelegateStub(onMsgStateChanged: { (message: ChatMessage, msgState: ChatMessage.State) in + let lastMessageStateTmp = msgState.rawValue + DispatchQueue.main.async { + self.lastMessageState = lastMessageStateTmp + } + }) + + if self.lastMessage != nil && self.chatMessageDelegate != nil { + self.lastMessage!.addDelegate(delegate: self.chatMessageDelegate!) + } + } + + func chatMessageRemoveDelegate() { + if self.lastMessage != nil && chatMessageDelegate != nil { + self.lastMessage!.removeDelegate(delegate: chatMessageDelegate!) + } + } + func getContentTextMessage(chatRoom: ChatRoom) { let lastMessage = chatRoom.lastMessageInHistory if lastMessage != nil { + if !(CoreContext.shared.imdnToEverybodyThreshold && !lastMessage!.isOutgoing) { + self.lastMessage = lastMessage + self.chatMessageAddDelegate() + } + var fromAddressFriend = lastMessage!.fromAddress != nil ? self.contactsManager.getFriendWithAddress(address: lastMessage!.fromAddress)?.name ?? nil : nil diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 9c43c5677..d7f188188 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -452,23 +452,19 @@ class ConversationViewModel: ObservableObject { } func getHistorySize() { - coreContext.doOnCoreQueue { _ in - if self.displayedConversation != nil { - let historySize = self.displayedConversation!.chatRoom.historyEventsSize - DispatchQueue.main.async { - self.displayedConversationHistorySize = historySize - } + if self.displayedConversation != nil { + let historySize = self.displayedConversation!.chatRoom.historyEventsSize + DispatchQueue.main.async { + self.displayedConversationHistorySize = historySize } } } func getUnreadMessagesCount() { - coreContext.doOnCoreQueue { _ in - if self.displayedConversation != nil { - let unreadMessagesCount = self.displayedConversation!.chatRoom.unreadMessagesCount - DispatchQueue.main.async { - self.displayedConversationUnreadMessagesCount = unreadMessagesCount - } + if self.displayedConversation != nil { + let unreadMessagesCount = self.displayedConversation!.chatRoom.unreadMessagesCount + DispatchQueue.main.async { + self.displayedConversationUnreadMessagesCount = unreadMessagesCount } } } @@ -490,44 +486,42 @@ class ConversationViewModel: ObservableObject { } func getParticipantConversationModel() { - coreContext.doOnCoreQueue { _ in - if self.displayedConversation != nil { - DispatchQueue.main.async { - self.isUserAdmin = false - self.participantConversationModelAdmin.removeAll() - self.participantConversationModel.removeAll() - } - self.displayedConversation!.chatRoom.participants.forEach { participant in - if participant.address != nil { - ContactAvatarModel.getAvatarModelFromAddress(address: participant.address!) { avatarResult in - let avatarModelTmp = avatarResult - if participant.isAdmin { - DispatchQueue.main.async { - self.participantConversationModelAdmin.append(avatarModelTmp) - self.participantConversationModel.append(avatarModelTmp) - } - } else { - DispatchQueue.main.async { - self.participantConversationModel.append(avatarModelTmp) - } + if self.displayedConversation != nil { + DispatchQueue.main.async { + self.isUserAdmin = false + self.participantConversationModelAdmin.removeAll() + self.participantConversationModel.removeAll() + } + self.displayedConversation!.chatRoom.participants.forEach { participant in + if participant.address != nil { + ContactAvatarModel.getAvatarModelFromAddress(address: participant.address!) { avatarResult in + let avatarModelTmp = avatarResult + if participant.isAdmin { + DispatchQueue.main.async { + self.participantConversationModelAdmin.append(avatarModelTmp) + self.participantConversationModel.append(avatarModelTmp) + } + } else { + DispatchQueue.main.async { + self.participantConversationModel.append(avatarModelTmp) } } } } - - if !self.displayedConversation!.isReadOnly { - if let currentUser = self.displayedConversation?.chatRoom.me, - let address = currentUser.address { - ContactAvatarModel.getAvatarModelFromAddress(address: address) { avatarResult in - let avatarModelTmp = avatarResult - DispatchQueue.main.async { - if currentUser.isAdmin { - self.isUserAdmin = true - self.participantConversationModelAdmin.append(avatarModelTmp) - } - self.participantConversationModel.append(avatarModelTmp) - self.myParticipantConversationModel = avatarModelTmp + } + + if !self.displayedConversation!.isReadOnly { + if let currentUser = self.displayedConversation?.chatRoom.me, + let address = currentUser.address { + ContactAvatarModel.getAvatarModelFromAddress(address: address) { avatarResult in + let avatarModelTmp = avatarResult + DispatchQueue.main.async { + if currentUser.isAdmin { + self.isUserAdmin = true + self.participantConversationModelAdmin.append(avatarModelTmp) } + self.participantConversationModel.append(avatarModelTmp) + self.myParticipantConversationModel = avatarModelTmp } } } @@ -549,12 +543,6 @@ class ConversationViewModel: ObservableObject { } func getMessages() { - self.getHistorySize() - self.getUnreadMessagesCount() - self.getParticipantConversationModel() - self.computeComposingLabel() - self.getEphemeralTime() - self.mediasToSend.removeAll() self.messageToReply = nil @@ -563,6 +551,12 @@ class ConversationViewModel: ObservableObject { self.attachments.removeAll() coreContext.doOnCoreQueue { _ in + self.getHistorySize() + self.getUnreadMessagesCount() + self.getParticipantConversationModel() + self.computeComposingLabel() + self.getEphemeralTime() + if self.displayedConversation != nil { let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30) @@ -1930,18 +1924,14 @@ class ConversationViewModel: ObservableObject { switch attachment.type { case .image: content.type = "image" - /* - case .audio: - content.type = "audio" - */ + case .audio: + content.type = "audio" case .video: content.type = "video" - /* - case .pdf: - content.type = "application" - case .plainText: - content.type = "text" - */ + case .pdf: + content.type = "application" + case .text: + content.type = "text" default: content.type = "file" } @@ -1953,25 +1943,21 @@ class ConversationViewModel: ObservableObject { if message != nil { - let path = FileManager.default.temporaryDirectory.appendingPathComponent((attachment.full.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")) - let newPath = URL(string: FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString - + (attachment.full.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")) - /* - let data = try Data(contentsOf: path) - let decodedData: () = try data.write(to: path) - */ - - do { - if FileManager.default.fileExists(atPath: newPath!.path) { - try FileManager.default.removeItem(atPath: newPath!.path) + let path = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.full.lastPathComponent) + if let newPath = URL(string: FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString + + (attachment.full.lastPathComponent)) { + do { + if FileManager.default.fileExists(atPath: newPath.path) { + try FileManager.default.removeItem(atPath: newPath.path) + } + try FileManager.default.moveItem(atPath: path.path, toPath: newPath.path) + + content.filePath = newPath.path + + message!.addFileContent(content: content) + } catch { + Log.error(error.localizedDescription) } - try FileManager.default.moveItem(atPath: path.path, toPath: newPath!.path) - - let filePathTmp = newPath?.absoluteString - content.filePath = String(filePathTmp!.dropFirst(7)) - message!.addFileContent(content: content) - } catch { - Log.error(error.localizedDescription) } } } catch { @@ -2027,10 +2013,9 @@ class ConversationViewModel: ObservableObject { if let newChatRoom = core.searchChatRoom(params: nilParams, localAddr: nil, remoteAddr: conversationModel.chatRoom.peerAddress, participants: nil) { if LinphoneUtils.getChatRoomId(room: newChatRoom) == conversationModel.id { self.addConversationDelegate(chatRoom: newChatRoom) - let conversation = ConversationModel(chatRoom: newChatRoom) DispatchQueue.main.async { withAnimation { - self.displayedConversation = conversation + self.displayedConversation = conversationModel } self.getMessages() } @@ -2596,31 +2581,29 @@ class ConversationViewModel: ObservableObject { } func getEphemeralTime() { - coreContext.doOnCoreQueue { _ in - if self.displayedConversation != nil { - - let lifetime = self.displayedConversation!.chatRoom.ephemeralLifetime - DispatchQueue.main.async { - switch lifetime { - case 60: - self.isEphemeral = true - self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_minute", comment: "") - case 3600: - self.isEphemeral = true - self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_hour", comment: "") - case 86400: - self.isEphemeral = true - self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_day", comment: "") - case 259200: - self.isEphemeral = true - self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_three_days", comment: "") - case 604800: - self.isEphemeral = true - self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_week", comment: "") - default: - self.isEphemeral = false - self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") - } + if self.displayedConversation != nil { + + let lifetime = self.displayedConversation!.chatRoom.ephemeralLifetime + DispatchQueue.main.async { + switch lifetime { + case 60: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_minute", comment: "") + case 3600: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_hour", comment: "") + case 86400: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_day", comment: "") + case 259200: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_three_days", comment: "") + case 604800: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_week", comment: "") + default: + self.isEphemeral = false + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") } } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index 7c362e3f1..009e2470d 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -198,6 +198,9 @@ class ConversationsListViewModel: ObservableObject { let model = ConversationModel(chatRoom: chatRoom) let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) + if index != nil { + self.conversationsList[index!].chatMessageRemoveDelegate() + } DispatchQueue.main.async { if index != nil { self.conversationsList.remove(at: index!) diff --git a/Linphone/Utils/FilePicker.swift b/Linphone/Utils/FilePicker.swift new file mode 100644 index 000000000..cb827643c --- /dev/null +++ b/Linphone/Utils/FilePicker.swift @@ -0,0 +1,111 @@ +/* + * 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 UniformTypeIdentifiers + +struct FilePicker: UIViewControllerRepresentable { + var onDocumentsPicked: ([URL]) -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(onDocumentsPicked: onDocumentsPicked) + } + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let picker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.item], asCopy: true) + picker.allowsMultipleSelection = true + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} + + class Coordinator: NSObject, UIDocumentPickerDelegate { + let onDocumentsPicked: ([URL]) -> Void + + init(onDocumentsPicked: @escaping ([URL]) -> Void) { + self.onDocumentsPicked = onDocumentsPicked + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + onDocumentsPicked(urls) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + onDocumentsPicked([]) + } + } + + static func convertToAttachmentArray(fromResults results: [URL], onComplete: @escaping ([Attachment]?, Error?) -> Void) { + var medias = [Attachment]() + + let dispatchGroup = DispatchGroup() + for urlFile in results { + dispatchGroup.enter() + do { + let mimeType = urlFile.mimeType() + if !mimeType.isEmpty { + let type = mimeType.components(separatedBy: "/").first ?? "" + let subtype = mimeType.components(separatedBy: "/").last ?? "" + let dataResult = try Data(contentsOf: urlFile) + + var typeTmp: AttachmentType = .other + switch type { + case "image": + typeTmp = urlFile.lastPathComponent.lowercased().hasSuffix("gif") ? .gif : .image + case "audio": + typeTmp = .audio + case "application": + typeTmp = subtype.lowercased() == "pdf" ? .pdf : .other + case "text": + typeTmp = .text + case "video": + typeTmp = .video + default: + typeTmp = .other + } + + if typeTmp == .video { + let urlImage = PhotoPicker.saveMedia(name: urlFile.lastPathComponent, data: dataResult, type: .video) + let urlThumbnail = PhotoPicker.getURLThumbnail(name: urlFile.lastPathComponent) + + if urlImage != nil { + let attachment = Attachment(id: UUID().uuidString, name: urlImage!.lastPathComponent, thumbnail: urlThumbnail, full: urlImage!, type: .video) + medias.append(attachment) + } + } else { + let urlImage = PhotoPicker.saveMedia(name: urlFile.lastPathComponent, data: dataResult, type: typeTmp) + + if urlImage != nil { + let attachment = Attachment(id: UUID().uuidString, name: urlImage!.lastPathComponent, url: urlImage!, type: typeTmp) + medias.append(attachment) + } + } + } + } catch { + + } + dispatchGroup.leave() + } + + dispatchGroup.notify(queue: .main) { + onComplete(medias, nil) + } + } +} diff --git a/Linphone/Utils/FileUtils.swift b/Linphone/Utils/FileUtils.swift index d81046753..d39a93193 100644 --- a/Linphone/Utils/FileUtils.swift +++ b/Linphone/Utils/FileUtils.swift @@ -32,6 +32,27 @@ class FileUtil: NSObject { case unknown } + public class func formUrlEncode(_ inputString: String) -> String { + // https://www.w3.org/TR/html5/sec-forms.html#application-x-www-form-urlencoded-encoding-algorithm + // Encode tous les caractères sauf *-._A-Za-z0-9, remplace les espaces par '+' + + guard !inputString.isEmpty else { + return inputString + } + + // Définir les caractères autorisés (non encodés) + var allowed = CharacterSet.alphanumerics + allowed.insert(charactersIn: "*-._") + + // Appliquer l'encodage pour les autres caractères + var encoded = inputString.addingPercentEncoding(withAllowedCharacters: allowed) ?? "" + + // Remplacer les espaces (déjà encodés en %20) par '+' + encoded = encoded.replacingOccurrences(of: "%20", with: "+") + + return encoded + } + public class func bundleFilePath(_ file: NSString) -> String? { return Bundle.main.path(forResource: file.deletingPathExtension, ofType: file.pathExtension) } diff --git a/Linphone/Utils/PhotoPicker.swift b/Linphone/Utils/PhotoPicker.swift index 16a309adf..9a29a570f 100644 --- a/Linphone/Utils/PhotoPicker.swift +++ b/Linphone/Utils/PhotoPicker.swift @@ -122,8 +122,7 @@ struct PhotoPicker: UIViewControllerRepresentable { static func saveMedia(name: String, data: Data, type: AttachmentType) -> URL? { do { - let path = FileManager.default.temporaryDirectory.appendingPathComponent((name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")) - + let path = FileManager.default.temporaryDirectory.appendingPathComponent(name) _ = try data.write(to: path) if type == .video { diff --git a/Linphone/en.lproj/Localizable.strings b/Linphone/en.lproj/Localizable.strings index 4ceebb58a..17ef6fd28 100644 --- a/Linphone/en.lproj/Localizable.strings +++ b/Linphone/en.lproj/Localizable.strings @@ -241,6 +241,10 @@ "conversation_reply_to_message_title" = "Replying to: "; "conversation_text_field_hint" = "Say something…"; "conversations_list_empty" = "No conversation for the moment…"; +"conversation_take_picture_label" = "Take picture"; +"conversation_pick_file_from_gallery_label" = "Open gallery"; +"conversation_pick_any_file_label" = "Pick file"; +"conversation_file_cant_be_opened_error_toast" = "File can't be opened!"; "debug_logs_copied_to_clipboard_toast" = "Debug logs copied to clipboard"; "Default" = "Default"; "Default mode" = "Default mode"; diff --git a/Linphone/fr.lproj/Localizable.strings b/Linphone/fr.lproj/Localizable.strings index 8e62e892d..48cab1d2a 100644 --- a/Linphone/fr.lproj/Localizable.strings +++ b/Linphone/fr.lproj/Localizable.strings @@ -241,6 +241,10 @@ "conversation_reply_to_message_title" = "En réponse à : "; "conversation_text_field_hint" = "Dites quelque chose…"; "conversations_list_empty" = "Aucune conversation pour le moment…"; +"conversation_take_picture_label" = "Prendre une photo"; +"conversation_pick_file_from_gallery_label" = "Ouvrir la gallerie"; +"conversation_pick_any_file_label" = "Choisir un fichier"; +"conversation_file_cant_be_opened_error_toast" = "Impossible d'ouvrir le fichier!"; "debug_logs_copied_to_clipboard_toast" = "Les journaux ont été ajoutés au presse-papier"; "Default" = "Default"; "Default mode" = "Default mode";