SwiftUI/Firebase:图库视图中的图像和视频不会自动更新

问题描述 投票:0回答:1

我正在开发一个聊天应用程序,使用 SwiftUI 作为前端,使用 Firebase Firestore 和 Firebase Storage 作为后端服务。我的应用程序有一个功能,用户可以在聊天中共享图像和视频,并且这些媒体项目应显示在单独的图库视图中(

ChatGalleryView
)。但是,我遇到了一个问题,即聊天中共享的图像和视频不会在图库视图中自动更新。

这里简要概述了数据的结构以及我的代码的相关部分:

数据结构:

  • 聊天消息存储在 Firestore 中名为

    chats
    的集合中,每个聊天都有一个子集合
    messages

  • 每条消息可以包含文本、图像或视频。图像 URL 和视频 URL 存储在

    messages
    字段下的
    content
    文档中,这是一个包含
    imageUrl
    videoUrl
    等字段的地图。

    尽管在

    ChatViewModel
    中设置侦听器以在将新图像或视频添加到聊天时获取并更新
    imageUrls
    videoUrls
    数组,但
    ChatGalleryView
    不会反映这些更新自动。

    我确保监听器已连接并正确获取数据,因为当共享新图像/视频时,我可以看到 URL 被打印到控制台。然而,这些数据似乎并没有传播到

    ChatGalleryView

    有人遇到过类似的问题或者能发现我可能做错了什么吗?如何确保在聊天中添加新媒体时

    ChatGalleryView
    自动更新?

    ´´´

    class ChatViewModel: ObservableObject {
    
        private var db = Firestore.firestore()
        private var messagesListener: ListenerRegistration?
        private var storage = Storage.storage()
    
        private let locationManager = CLLocationManager()
    
    
        @Published var messages: [ChatMessage] = []
        @Published var showingPermissionAlert: Bool = false
        @Published var isImagePickerPresented = false
        @Published var chats: [ChatModel] = []
        @Published var userNickname: String?
        @Published var isContactPickerPresented: Bool = false
        @Published var replyingToMessage: ChatMessage? = nil
        @Published var scrollToMessageId: String?
        @Published var imageUrls: [String] = []
        @Published var videoUrls: [String] = []
    
      func setupMessagesListener(chatId: String) {
            let messagesRef = db.collection("chats").document(chatId).collection("messages")
            messagesListener = messagesRef.order(by: "sentAt", descending: false).addSnapshotListener { [weak self] (querySnapshot, error) in
                guard let self = self, let snapshot = querySnapshot else {
                    print("Error listening for message updates: \(error?.localizedDescription ?? "No error")")
                    return
                }
    
                snapshot.documentChanges.forEach { change in
                    let data = change.document.data()
    
                    // Asegúrate de acceder al mapa 'content' para obtener 'imageUrl' y 'videoUrl'
                    if let content = data["content"] as? [String: Any] {
                        if let imageUrl = content["imageUrl"] as? String, !imageUrl.isEmpty {
                            print("Image URL found: \(imageUrl)")
                            self.updateImageUrls(with: imageUrl, messageId: change.document.documentID, added: change.type == .added)
                        }
                        if let videoUrl = content["videoUrl"] as? String, !videoUrl.isEmpty {
                            print("Video URL found: \(videoUrl)")
                            self.updateVideoUrls(with: videoUrl, messageId: change.document.documentID, added: change.type == .added)
                        }
                    }
                }
            }
        }
    
    
        private func updateImageUrls(with imageUrl: String, messageId: String, added: Bool) {
            DispatchQueue.main.async {
                if added, !self.imageUrls.contains(imageUrl) {
                    print("Adding image URL: \(imageUrl) for message ID: \(messageId)")
                    self.imageUrls.append(imageUrl)
                } else if !added {
                    print("Removing image URL: \(imageUrl) for message ID: \(messageId)")
                    self.imageUrls.removeAll { $0 == imageUrl }
                }
            }
        }
    
        private func updateVideoUrls(with videoUrl: String, messageId: String, added: Bool) {
            DispatchQueue.main.async {
                if added, !self.videoUrls.contains(videoUrl) {
                    print("Adding video URL: \(videoUrl) for message ID: \(messageId)")
                    self.videoUrls.append(videoUrl)
                } else if !added {
                    print("Removing video URL: \(videoUrl) for message ID: \(messageId)")
                    self.videoUrls.removeAll { $0 == videoUrl }
                }
            }
        }
    
    

导入 SwiftUI 导入AVKit

结构 ChatGalleryView:查看 {

var chatId: String
@ObservedObject var viewModel: ChatViewModel


enum GalleryTab {
    case images, videos
}

@State private var selectedTab: GalleryTab = .images

var body: some View {
    VStack {
        Picker("Select", selection: $selectedTab) {
            Text("Images").tag(GalleryTab.images)
            Text("Videos").tag(GalleryTab.videos)
        }
        .pickerStyle(SegmentedPickerStyle())
        .padding()
        
        switch selectedTab {
            
        case .images:
            
            GalleryImagesView(imageUrls: viewModel.imageUrls, viewModel: viewModel, chatId: chatId)
            
        case .videos:
            
            GalleryVideosView(videoUrls: viewModel.videoUrls, viewModel: viewModel, chatId: chatId)
        }
        
        Spacer()
    }
    .background(LinearGradient(gradient: Gradient(colors: [Color.white, Color.blue.opacity(0.3)]), startPoint: .top, endPoint: .bottom))
    .navigationBarTitle("Gallery", displayMode: .inline)
    .onAppear {
        print("Cargando galería con \(viewModel.imageUrls.count) imágenes y \(viewModel.videoUrls.count) vídeos")
        print("URLs de imágenes: \(viewModel.imageUrls)")
        print("URLs de vídeos: \(viewModel.videoUrls)")
    }
}

}

结构GalleryImagesView:视图{ 让 imageUrls: [字符串] @ObservedObject var viewModel: ChatViewModel var chatId:字符串

var body: some View {
    ScrollView {
        LazyVStack {
            ForEach(imageUrls, id: \.self) { imageUrl in
                AsyncImage(url: URL(string: imageUrl)) { phase in
                    switch phase {
                    case .success(let image):
                        image.resizable()
                             .aspectRatio(contentMode: .fit)
                        
                    default:
                        ProgressView()
                    }
                }
                .contextMenu { // Uso correcto de contextMenu
                    Button(action: {
                        viewModel.downloadImage(imageUrl, chatId: chatId)
                    }) {
                        Text(NSLocalizedString("Download", comment: "Download action"))
                        Image(systemName: "arrow.down.circle")
                    }
                    Button(action: {
                        viewModel.deleteImageFromChat(chatId: chatId, imageUrl: imageUrl)
                    }) {
                        Text(NSLocalizedString("Delete from Chat", comment: "Delete action"))
                        Image(systemName: "trash")
                    }
                }
            }
        }
    }
}

}

结构GalleryVideosView:查看{ 让 videoUrls: [字符串] @ObservedObject var viewModel: ChatViewModel var chatId:字符串

var body: some View {
    ScrollView {
        LazyVStack {
            ForEach(videoUrls, id: \.self) { videoUrl in
                if let url = URL(string: videoUrl) {
                    VideoPlayer(player: AVPlayer(url: url))
                        .frame(height: 200)
                        .contextMenu { // Uso correcto de contextMenu
                            Button(action: {
                                viewModel.downloadVideo(videoUrl, chatId: chatId)
                            }) {
                                Text(NSLocalizedString("Download", comment: "Download action"))
                                Image(systemName: "arrow.down.circle")
                            }
                            Button(action: {
                                viewModel.deleteVideoFromChat(chatId: chatId, videoUrl: videoUrl)
                            }) {
                                Text(NSLocalizedString("Delete from Chat", comment: "Delete action"))
                                Image(systemName: "trash")
                            }
                        }
                }
            }
        }
    }
}

}


New pictures uodated: [enter image description here][1] [enter image description here][2] [enter image description here][3] [enter image description here][4] [enter image description here][5] [enter image description here][6]


[1]: https://i.stack.imgur.com/2U25n.png
[2]: https://i.stack.imgur.com/f12tF.png
[3]: https://i.stack.imgur.com/4jGj6.png
[4]: https://i.stack.imgur.com/XWChv.png
[5]: https://i.stack.imgur.com/UJajJ.png
[6]: https://i.stack.imgur.com/iCWNq.png
swift xcode firebase swiftui uikit
1个回答
0
投票
  Here you are the view where the StateObject remains:
    
    ´´´
    
    import SwiftUI
    import UIKit
    import FirebaseAuth
    import FirebaseFirestore
    import AVKit
    import PhotosUI
    
    class MessageInputState: ObservableObject {
        @Published var messageText: String = ""
        @Published var isUploadingVideo: Bool = false
        
    }
    
    struct ChatView: View {
        @StateObject var viewModel: ChatViewModel

        @EnvironmentObject var userSession: UserSession
        
        var chatId: String
        
        @ObservedObject var messageState = MessageInputState()
        
        @State private var pickerResult: [UIImage] = []
        @State private var isImagePickerPresented: Bool = false
        @State private var showMoreOptions = false
        @State private var videoUrl: URL?
        @State private var isVideoPickerPresented: Bool = false
        @State private var userName: String = ""
        @State private var showAlert: Bool = false
        @State private var isAliasModalPresented: Bool = false
        @State private var showOptionsMenu = false
        @State private var nicknameToAdd: String = ""
        @State private var isNicknameDialogPresented: Bool = false
        @State private var userNickname: String?
        @State private var isAddParticipantViewPresented: Bool = false
        @State private var isGalleryViewPresented: Bool = false
        @State private var showOptionsView = false
        @State private var showingChatOptions = false
         
        var chatGroupName: String = "Chat"
        
        @State var connectedContacts: [String]?
        
        init(chatId: String, userSession: UserSession) {
            self.chatId = chatId
            self._viewModel = StateObject(wrappedValue: ChatViewModel(userSession: userSession))
            self.userNickname = userSession.nickname
            if let nickname = userSession.nickname {
                self.userName = nickname
            } else {
                self.userName = NSLocalizedString("unknown", comment: "Default username when unknown")
                print(NSLocalizedString("ChatView iniciado con chatId: ", comment: "Log message") + "\(chatId)")
                print(NSLocalizedString("Chats disponibles en viewModel: ", comment: "Log message") + "\(viewModel.chats)")
            }
        }
        
        var body: some View {
            NavigationView {
                VStack {
                    
                    ScrollView {
                        ForEach(viewModel.messages) { message in
                            Group {
                                if message.state == .deleted {
                                    Text("Message deleted / Mensaje borrado")
                                        .italic()
                                        .foregroundColor(.gray)
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                } else {
                                    ChatMessageView(message: message, chatId: chatId)
                                        .padding(.horizontal, 10)
                                        .padding(.vertical, 5)
                                        .environmentObject(viewModel)
                                        .contextMenu {
                                            if message.state != .deleted {
                                                Button(action: {
                                                    viewModel.startReplying(to: message)
                                                }) {
                                                    Label("Reply", systemImage: "arrowshape.turn.up.left")
                                                }
                                            }
                                            Button(role: .destructive, action: {
                                                viewModel.deleteMessage(chatId: chatId, messageId: message.id ?? "")
                                            }) {
                                                Label("Delete", systemImage: "trash")
                                            }
                                        }
                                }
                            }
                        }
                    }
                    
                    replyIndicator.transition(.slide).animation(.easeInOut, value: viewModel.replyingToMessage != nil)
                    messageInputSection.padding(.top, 5)
                }
                .navigationTitle(NSLocalizedString("Chat", comment: "Navigation title for chat view"))
                
                .navigationBarTitleDisplayMode(.inline)
                .actionSheet(isPresented: $showOptionsMenu) {
                    ActionSheet(
                        title: Text(NSLocalizedString("Options", comment: "Options")),
                        buttons: [
                            .default(Text(NSLocalizedString("Chat Options", comment: "Chat Options"))) {
                                // Aquí puedes cambiar el estado para mostrar la vista de opciones del chat
                                showingChatOptions = true
                            },
                            .default(Text(NSLocalizedString("Add Photo", comment: "Add Photo"))) {
                                self.isImagePickerPresented = true
                            },
                            .default(Text(NSLocalizedString("Add Video", comment: "Add Video"))) {
                                self.isVideoPickerPresented = true
                            },
                            .destructive(Text(NSLocalizedString("Leave Chat", comment: "Leave Chat"))) {
                                showAlert = true
                            },
                            .cancel()
                        ]
                    )
                }
                .sheet(isPresented: $showingChatOptions) {
                    ChatOptionsView(chatId: chatId, viewModel: viewModel)
                }
                .alert(isPresented: $showAlert) {
                    Alert(
                        title: Text(NSLocalizedString("Confirm Leave", comment: "Confirm Leave")),
                        message: Text(NSLocalizedString("Are you sure you want to leave the chat?", comment: "Are you sure you want to leave the chat?")),
                        primaryButton: .destructive(Text(NSLocalizedString("Leave", comment: "Leave"))) {
                            // Implementar lógica para salir del chat aquí
                        },
                        secondaryButton: .cancel()
                    )
                }
                
                .onAppear {
                    self.userName = userSession.nickname ?? NSLocalizedString("unknown", comment: "Fallback username when unknown")
                    viewModel.setupMessagesListener(chatId: chatId)
                    viewModel.fetchMessages(chatId: chatId)
                    viewModel.fetchChatParticipants(chatId: chatId) { participants in
                        self.connectedContacts = participants
                    }
    
                    
                    viewModel.openChat(chatId: chatId)
                }
                .sheet(isPresented: $showOptionsView) {
                    ChatOptionsView(chatId: chatId, viewModel: viewModel)
                }
                .sheet(isPresented: $isImagePickerPresented, onDismiss: handleImageSelection) {
                    PhotoPicker(pickerResult: $pickerResult, isPresented: $isImagePickerPresented)
                }
                
                .sheet(isPresented: $isVideoPickerPresented, onDismiss: handleVideoSelection) {
                    VideoPicker(videoUrl: $videoUrl, isPresented: $isVideoPickerPresented)
                }
                
                .sheet(isPresented: $isAddParticipantViewPresented) {
                    AddParticipantView(viewModel: viewModel, chatId: chatId)
                }
                
                .sheet(isPresented: $isGalleryViewPresented) {
                    ChatGalleryView(chatId: chatId, viewModel: viewModel)
                }
                
            }
            
        }
        
        private func sendMessage() {
            print("ChatView - sendMessage - userName: \(userName)")
            if userName.isEmpty {
                showAlert = true
                return
            }
            
            let messageText = messageState.messageText.trimmingCharacters(in: .whitespacesAndNewlines)
            if !messageText.isEmpty {
                let textMessageContent = MessageContent(text: messageText, imageUrl: nil, videoUrl: nil)
                
                // Imprime el valor de replyToMessageId antes de enviar
                let replyToMessageId = viewModel.replyingToMessage?.id // Actualizado para usar replyingToMessage
                print("Intentando enviar mensaje con replyToMessageId: \(String(describing: replyToMessageId))")
                
                viewModel.sendMessage(chatId: chatId, messageContent: textMessageContent, senderId: Auth.auth().currentUser?.uid ?? "unknown", senderName: userName) { success, error in
                    if success {
                        // Imprime para confirmar que el mensaje se envió correctamente
                        print("Message sent successfully with replyToMessageId: \(String(describing: replyToMessageId))")
                        
                        // Asegúrate de que el indicador de respuesta en la interfaz de usuario se resetee
                        self.viewModel.cancelReplying()
                        
                        // Limpia el campo de texto después de enviar
                        self.messageState.messageText = ""
                        
                        // Añadir un mensaje de confirmación aquí si es necesario
                        print("replyToMessageId se ha reseteado y el mensaje se ha enviado correctamente.")
                    } else {
                        print("Error sending message: \(error?.localizedDescription ?? "Error desconocido")")
                    }
                }
            } else {
                print("No hay texto para enviar.")
            }
            
            // Manejar automáticamente la selección de imágenes o vídeos
            handleMediaSelection()
        }
        
        private func sendUpdatedMessage(messageContent: MessageContent) {
            // Utiliza la propiedad replyingToMessage.id si está presente
            let replyToMessageId = viewModel.replyingToMessage?.id
            viewModel.sendMessage(chatId: chatId, messageContent: messageContent, senderId: Auth.auth().currentUser?.uid ?? "unknown", senderName: userName) { success, error in
                if success {
                    print("Mensaje enviado con éxito con replyToMessageId: \(String(describing: replyToMessageId))")
                    self.viewModel.cancelReplying() // Resetear el estado de respuesta
                } else {
                    print("Error al enviar mensaje: \(error?.localizedDescription ?? "Error desconocido")")
                }
            }
        }
        
        // Esta función ahora llama directamente a `sendUpdatedMessage`
        private func finalizeMessageSending(messageContent: MessageContent) {
            sendUpdatedMessage(messageContent: messageContent)
        }
        
        func uploadAndSendImages(images: [UIImage]) {
            var imageUrls = [String]()
            let group = DispatchGroup()
            
            for image in images {
                group.enter()
                viewModel.uploadImage(image) { uploadedImageUrl in
                    imageUrls.append(uploadedImageUrl)
                    group.leave()
                }
            }
            
            group.notify(queue: .main) {
                // Envío de un único mensaje con todas las URLs de las imágenes
                let messageContent = MessageContent(text: nil, imageUrl: imageUrls, videoUrl: nil, contact: nil)
                self.sendUpdatedMessage(messageContent: messageContent)
            }
        }
        
        
        func handleMediaSelection() {
            // Manejar la subida y envío de imágenes
            if !pickerResult.isEmpty {
                uploadAndSendImages(images: pickerResult)
                self.pickerResult.removeAll() // Limpiar después de enviar
            }
            
            // Manejar la subida y envío de vídeo
            if let videoUrl = videoUrl {
                uploadVideoAndSendMessage(videoUrl: videoUrl)
            }
        }
        
        // Maneja la selección de imagen
        func handleImageSelection() {
            if !pickerResult.isEmpty {
                uploadAndSendImages(images: pickerResult)
                self.pickerResult.removeAll() // Limpiar después de enviar
            }
        }
        // Maneja la selección de vídeo
        func handleVideoSelection() {
            guard let videoUrl = videoUrl else { return }
            messageState.isUploadingVideo = true // Indicar que la carga ha comenzado
            
            viewModel.uploadVideoAfterConversion(videoUrl) { [self] result in
                messageState.isUploadingVideo = false // Resetear el estado de carga independientemente del resultado
                
                switch result {
                case .success(let uploadedVideoUrl):
                    print("Video cargado y disponible en: \(uploadedVideoUrl)")
                    let videoMessageContent = MessageContent(text: nil, imageUrl: nil, videoUrl: [uploadedVideoUrl])
                    sendUpdatedMessage(messageContent: videoMessageContent)
                    self.videoUrl = nil // Limpiar después de enviar
                case .failure(let error):
                    print("Error al cargar el video: \(error.localizedDescription)")
                    // Manejar el error, por ejemplo, mostrando un mensaje al usuario.
                }
            }
        }
        
        
        private func uploadImages(images: [UIImage], completion: @escaping ([String]) -> Void) {
            var uploadedUrls = [String]()
            let uploadGroup = DispatchGroup()
            
            for image in images {
                uploadGroup.enter()
                viewModel.uploadImage(image) { imageUrl in
                    print("Imagen subida con URL: \(imageUrl)")
                    uploadedUrls.append(imageUrl)
                    uploadGroup.leave()
                }
            }
            
            uploadGroup.notify(queue: .main) {
                completion(uploadedUrls)
            }
        }
        
        private func uploadVideoAndSendMessage(videoUrl: URL) {
            viewModel.uploadVideo(videoUrl) { result in
                switch result {
                case .success(let uploadedVideoUrl):
                    print("Video subido con URL: \(uploadedVideoUrl)")
                    let videoMessageContent = MessageContent(text: nil, imageUrl: nil, videoUrl: [uploadedVideoUrl])
                    self.sendUpdatedMessage(messageContent: videoMessageContent)
                    self.videoUrl = nil // Limpiar después de enviar
                    
                case .failure(let error):
                    print("Error al subir el video: \(error.localizedDescription)")
                    // Manejar el error, por ejemplo, mostrando un mensaje al usuario
                }
            }
        }
        
        private var messageInputSection: some View {
            HStack {
                Button(action: { showOptionsMenu.toggle() }) {
                    Image(systemName: "plus.circle.fill")
                        .resizable()
                        .frame(width: 30, height: 30)
                        .foregroundColor(.blue)
                }
                
                TextField(NSLocalizedString("Message...", comment: "Placeholder text for message input field"), text: $messageState.messageText)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(10)
                    .background(Color.white)
                    .cornerRadius(10)
                    .shadow(color: Color.cyan.opacity(0.5), radius: 3, x: 0, y: 3)
                
                if messageState.isUploadingVideo {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle())
                        .padding(10)
                } else {
                    Button(action: sendMessage) {
                        Image(systemName: "paperplane.fill")
                            .foregroundColor(.blue)
                            .padding(10)
                    }
                    .disabled(messageState.messageText.isEmpty && !messageState.isUploadingVideo) // Deshabilita el botón si no hay texto o un video se está cargando
                }
            }
            .padding(.horizontal)
        }
    }
        
    struct ChatMessageView: View {
        
        @EnvironmentObject var viewModel: ChatViewModel
        @State private var showingDeleteConfirmation = false
        @State private var messageIdToDelete: String?
        @State private var selectedVideoUrl: URL?
        @State private var showVideoPlayer = false
        @State private var selectedImageUrl: URL?
        @State private var showImageViewer: Bool = false
        
        let message: ChatMessage
        let chatId: String
        
        private var dateFormatter: DateFormatter {
            let formatter = DateFormatter()
            formatter.dateStyle = .none
            formatter.timeStyle = .short
            return formatter
        }
        
        var body: some View {
            VStack(alignment: .leading, spacing: 4) {
                HStack {
                    if message.senderId == Auth.auth().currentUser?.uid {
                        Spacer()
                    }
                    
                    VStack(alignment: .leading, spacing: 4) {
                        // Aquí, directamente verificas si el mensaje actual es una respuesta usando la lógica adaptada
                        if let replyingToMessage = viewModel.replyingToMessage, message.id == replyingToMessage.id {
                            QuoteView(message: replyingToMessage)
                                .background(Color(red: 0.95, green: 0.95, blue: 0.95)) // Un gris muy suave personalizado
                                .cornerRadius(8)
                                .onAppear {
                                    print("Replying to message: \(replyingToMessage.content.text ?? "")")
                                }
                        } else {
                            EmptyView()
                                .onAppear {
                                    print("This message is not a reply or replyToMessageId is empty.")
                                }
                        }
                        
                        // Nombre del emisor del mensaje actual
                        Text(message.senderName)
                            .font(.caption)
                            .foregroundColor(.secondary)
                        
                        // Contenido del mensaje actual
                        messageContentView(message: message)
                        
                        // Hora de envío del mensaje actual
                        Text("\(message.sentAt, formatter: dateFormatter)")
                            .font(.caption2)
                            .foregroundColor(.gray)
                    }
                    .padding(.all, 10)
                    .background(Color.white.opacity(0.7))
                    .cornerRadius(10)
                    .shadow(radius: 1)
                    
                    if message.senderId != Auth.auth().currentUser?.uid {
                        Spacer()
                    }
                }
            }
            .contextMenu {
                Button(NSLocalizedString("Reply", comment: "Botón para responder a un mensaje")) {
                    viewModel.startReplying(to: message)
                }
                Button(NSLocalizedString("Delete", comment: "Botón para eliminar un mensaje"), role: .destructive) {
                    messageIdToDelete = message.id
                    showingDeleteConfirmation = true
                }
            }
            .alert(NSLocalizedString("Are you sure you want to delete this message?", comment: "Confirmación de eliminación de mensaje"), isPresented: $showingDeleteConfirmation) {
                Button(NSLocalizedString("Delete", comment: "Confirmar eliminación"), role: .destructive) {
                    if let messageIdToDelete = messageIdToDelete {
                        viewModel.deleteMessage(chatId: chatId, messageId: messageIdToDelete)
                    }
                }
                Button(NSLocalizedString("Cancel", comment: "Cancelar eliminación"), role: .cancel) { }
            }
            .sheet(isPresented: $showVideoPlayer) {
                if let videoUrl = selectedVideoUrl {
                    VideoPlayer(player: AVPlayer(url: videoUrl))
                        .edgesIgnoringSafeArea(.all)
                        .onDisappear {
                            AVPlayer(url: videoUrl).pause()
                        }
                }
            }
            
            .sheet(isPresented: $showImageViewer) {
                if let selectedImageUrl = selectedImageUrl {
                    AsyncImage(url: selectedImageUrl) { phase in
                        switch phase {
                        case .success(let image):
                            image.resizable()
                                .aspectRatio(contentMode: .fit)
                                .edgesIgnoringSafeArea(.all)
                        default:
                            ProgressView()
                        }
                    }
                }
            }
        }
        
        private func messageContentView(message: ChatMessage) -> some View {
            if message.isDeleted {
                // Mensaje borrado
                return AnyView(
                    Text("Mensaje borrado / Message deleted")
                        .foregroundColor(.gray)
                        .font(.footnote).italic()
                        .padding(10)
                        .background(Color.red.opacity(0.3))
                        .cornerRadius(15)
                )
            } else {
                // Contenido del mensaje no borrado
                return AnyView(
                    VStack(alignment: .leading, spacing: 4) {
                        // Verificar si el mensaje es una respuesta
                        if let replyToMessageId = message.replyToMessageId, !replyToMessageId.isEmpty,
                           let originalMessage = viewModel.messages.first(where: { $0.id == replyToMessageId }) {
                            QuoteView(message: originalMessage)
                                .padding(.bottom, 4)
                                .background(Color(red: 0.95, green: 0.95, blue: 0.95))
                                .cornerRadius(8)
                                .padding(.leading, 10)
                        }
                        
                        // Contenido del mensaje
                        messageContentDetails(message: message)
                    }
                )
            }
        }
        
        @ViewBuilder
        private func messageContentDetails(message: ChatMessage) -> some View {
            // Texto del mensaje
            if let text = message.content.text, !text.isEmpty {
                Text(text)
                    .padding(10)
                    .background(message.senderId == Auth.auth().currentUser?.uid ? Color.blue : Color.gray)
                    .foregroundColor(.white)
                    .cornerRadius(15)
            }
            
            // Imágenes del mensaje
            if let imageUrls = message.content.imageUrl, !imageUrls.isEmpty {
                imageCarousel(imageUrls: imageUrls)
            }
            
            // Videos del mensaje
            if let videoUrls = message.content.videoUrl, !videoUrls.isEmpty {
                videoCarousel(videoUrls: videoUrls)
            }
        }
        
        @ViewBuilder
        private func imageCarousel(imageUrls: [String]) -> some View {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(imageUrls, id: \.self) { imageUrlString in
                        if let url = URL(string: imageUrlString) {
                            Button(action: {
                                self.selectedImageUrl = url
                                self.showImageViewer = true
                            }) {
                                AsyncImage(url: url) { phase in
                                    switch phase {
                                    case .empty:
                                        ProgressView()
                                    case .success(let image):
                                        image.resizable()
                                            .aspectRatio(contentMode: .fit)
                                            .frame(width: 200, height: 200)
                                            .cornerRadius(15)
                                    case .failure:
                                        Image(systemName: "photo")
                                    @unknown default:
                                        EmptyView()
                                    }
                                }
                            }
                        }
                    }
                }
            }
            .frame(height: 200) // Altura fija para el carrusel
        }
        
        @ViewBuilder
        private func videoCarousel(videoUrls: [String]) -> some View {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(videoUrls, id: \.self) { videoUrlString in
                        if let url = URL(string: videoUrlString) {
                            Button(action: {
                                self.selectedVideoUrl = url
                                self.showVideoPlayer = true
                            }) {
                                VideoThumbnailView(videoUrl: url)
                                    .frame(width: 200, height: 200)
                                    .cornerRadius(15)
                            }
                        }
                    }
                }
            }
            .frame(height: 200) // Altura fija para el carrusel de videos
        }
    }

´´´
© www.soinside.com 2019 - 2024. All rights reserved.