diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 0bf43c8c5..b7b73212a 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -163,6 +163,7 @@ D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */; }; D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; }; D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */; }; + D7E2E6A12CE5F8850080DA0D /* QuickLookPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */; }; D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF22B9875C20009A2BC /* Message.swift */; }; D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF42B9876ED0009A2BC /* Attachment.swift */; }; D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; }; @@ -355,6 +356,7 @@ D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFragment.swift; sourceTree = ""; }; D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = ""; }; D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupViewWithTextField.swift; sourceTree = ""; }; + D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreview.swift; sourceTree = ""; }; D7E6ADF22B9875C20009A2BC /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; D7E6ADF42B9876ED0009A2BC /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = ""; }; @@ -517,6 +519,7 @@ C67586AF2C09F247002E77BF /* URIHandler.swift */, C6A5A9462C10B64A0070FEA4 /* SingleSignOn */, D79F2D092C47F4BF0038FA07 /* TouchFeedback.swift */, + D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */, ); path = Utils; sourceTree = ""; @@ -1216,6 +1219,7 @@ 66F08C892C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift in Sources */, D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */, + D7E2E6A12CE5F8850080DA0D /* QuickLookPreview.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */, D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */, diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index ab3ea8e76..920f71124 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -39,6 +39,9 @@ struct ChatBubbleView: View { @State private var timer: Timer? @State private var ephemeralLifetime: String = "" + @State private var selectedAttachment: Bool = false + @State private var selectedAttachmentIndex: Int = 0 + var body: some View { HStack { if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage { @@ -488,6 +491,9 @@ struct ChatBubbleView: View { } UIApplication.shared.endEditing() } + .fullScreenCover(isPresented: $selectedAttachment) { + QuickLookFullScreenView(conversationViewModel: conversationViewModel, currentIndex: $selectedAttachmentIndex) + } } func containsDuplicates(strings: [String]) -> Bool { @@ -554,6 +560,10 @@ struct ChatBubbleView: View { } .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } else { AsyncImage(url: eventLogMessage.message.attachments.first!.thumbnail) { phase in switch phase { @@ -583,17 +593,29 @@ struct ChatBubbleView: View { .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) .id(UUID()) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } } else if eventLogMessage.message.attachments.first!.type == .gif { if #available(iOS 16.0, *) { GifImageView(eventLogMessage.message.attachments.first!.thumbnail) .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } else { GifImageView(eventLogMessage.message.attachments.first!.thumbnail) .id(UUID()) .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } } } @@ -636,13 +658,17 @@ struct ChatBubbleView: View { .frame(width: geometryProxy.size.width - 110, height: 100) .background(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } } else if eventLogMessage.message.attachments.count > 1 { let isGroup = conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup LazyVGrid(columns: [ GridItem(.adaptive(minimum: 120), spacing: 1) ], spacing: 3) { - ForEach(eventLogMessage.message.attachments) { attachment in + ForEach(eventLogMessage.message.attachments, id: \.id) { attachment in ZStack { Rectangle() .fill(Color(.white)) @@ -668,6 +694,10 @@ struct ChatBubbleView: View { ProgressView() } .layoutPriority(-1) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: attachment) + selectedAttachment.toggle() + } } else { AsyncImage(url: attachment.thumbnail) { image in ZStack { @@ -689,6 +719,10 @@ struct ChatBubbleView: View { } .id(UUID()) .layoutPriority(-1) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: attachment) + selectedAttachment.toggle() + } } } .clipShape(RoundedRectangle(cornerRadius: 4)) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index b74b31390..69405473a 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -103,6 +103,8 @@ class ConversationViewModel: ObservableObject { @Published var isPlaying = false @Published var progress: Double = 0.0 + @Published var attachments: [Attachment] = [] + struct SheetCategory: Identifiable { let id = UUID() let name: String @@ -392,6 +394,8 @@ class ConversationViewModel: ObservableObject { self.conversationInfoPopupText = displayedConversation?.subject ?? "" + self.attachments.removeAll() + coreContext.doOnCoreQueue { _ in if self.displayedConversation != nil { let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30) @@ -454,6 +458,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -471,6 +480,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -679,6 +691,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -696,6 +713,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -902,6 +922,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -919,6 +944,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -1203,6 +1231,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -1220,6 +1253,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -2220,6 +2256,10 @@ class ConversationViewModel: ObservableObject { } } } + + func getAttachmentIndex(attachment: Attachment) -> Int { + return self.attachments.firstIndex(where: {$0.id == attachment.id}) ?? 0 + } } // swiftlint:enable line_length // swiftlint:enable type_body_length diff --git a/Linphone/Utils/QuickLookPreview.swift b/Linphone/Utils/QuickLookPreview.swift index e69de29bb..d0c081b46 100644 --- a/Linphone/Utils/QuickLookPreview.swift +++ b/Linphone/Utils/QuickLookPreview.swift @@ -0,0 +1,83 @@ +/* + * 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 QuickLook + +struct QuickLookFullScreenView: View { + @Environment(\.presentationMode) var presentationMode + + @ObservedObject var conversationViewModel: ConversationViewModel + + @Binding var currentIndex: Int + + var body: some View { + NavigationView { + QuickLookPreview(fileURLs: conversationViewModel.attachments.map { $0.full }, startIndex: currentIndex) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(conversationViewModel.attachments.first?.name ?? "File error") + .lineLimit(1) + .truncationMode(.middle) + .font(.headline) + } + } + .navigationBarItems(trailing: Button("Close") { + presentationMode.wrappedValue.dismiss() + }) + } + } +} + +struct QuickLookPreview: UIViewControllerRepresentable { + let fileURLs: [URL] + let startIndex: Int + + func makeUIViewController(context: Context) -> QLPreviewController { + let previewController = QLPreviewController() + previewController.dataSource = context.coordinator + previewController.currentPreviewItemIndex = startIndex // Définir l’index de départ + return previewController + } + + func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) { + // No update needed + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, QLPreviewControllerDataSource { + var parent: QuickLookPreview + + init(_ parent: QuickLookPreview) { + self.parent = parent + } + + func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + return parent.fileURLs.count + } + + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + return parent.fileURLs[index] as QLPreviewItem + } + } +}