From 0d74e6651aec2c76060bac5c619ca61a1f2a0f60 Mon Sep 17 00:00:00 2001 From: "benoit.martins" Date: Tue, 12 Mar 2024 17:24:49 +0100 Subject: [PATCH] Add gifs support --- Linphone/Localizable.xcstrings | 6 - .../Fragments/ChatBubbleView.swift | 187 ++++++++++-------- .../Fragments/ConversationFragment.swift | 24 ++- .../Main/Conversations/Fragments/UIList.swift | 8 +- .../Main/Conversations/Model/Attachment.swift | 1 + .../ViewModel/ConversationViewModel.swift | 26 +-- 6 files changed, 131 insertions(+), 121 deletions(-) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 55e8fd566..6bb5ae918 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -244,9 +244,6 @@ }, "Contacts" : { - }, - "Content" : { - }, "Continue" : { @@ -591,9 +588,6 @@ }, "This contact will be deleted definitively." : { - }, - "Title" : { - }, "TLS" : { diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index f0c10a683..c7dd24d4b 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -18,76 +18,17 @@ */ import SwiftUI +import WebKit struct ChatBubbleView: View { @ObservedObject var conversationViewModel: ConversationViewModel - //let index: IndexPath - let message: Message - var body: some View { - /* - if index < conversationViewModel.conversationMessagesList.count - && conversationViewModel.conversationMessagesList[index].eventLog.chatMessage != nil { - VStack { - if index == 0 && conversationViewModel.displayedConversationHistorySize > conversationViewModel.conversationMessagesList.count { - //if index % 30 == 29 && conversationViewModel.displayedConversationHistorySize > conversationViewModel.conversationMessagesList.count { - ProgressView() - .frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center) - .id(UUID()) - } - - HStack { - if conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing { - Spacer() - } - - VStack { - Text(conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.utf8Text ?? "") - .foregroundStyle(Color.grayMain2c700) - .default_text_style(styleSize: 16) - } - .padding(.all, 15) - .background(conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100) - .clipShape(RoundedRectangle(cornerRadius: 16)) - - if !conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing { - Spacer() - } - } - .padding(.leading, conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? 40 : 0) - .padding(.trailing, !conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? 40 : 0) - } - } - if conversationViewModel.conversationMessagesSection.count > index.section && conversationViewModel.conversationMessagesSection[index.section].rows.count > index.row { - VStack { - HStack { - if message.isOutgoing { - Spacer() - } - - VStack { - Text(message.text - ) - .foregroundStyle(Color.grayMain2c700) - .default_text_style(styleSize: 16) - } - .padding(.all, 15) - .background(message.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100) - .clipShape(RoundedRectangle(cornerRadius: 16)) - - if !message.isOutgoing { - Spacer() - } - } - .padding(.leading, message.isOutgoing ? 40 : 0) - .padding(.trailing, !message.isOutgoing ? 40 : 0) - } - } - */ - + let geometryProxy: GeometryProxy + + var body: some View { VStack { HStack { if message.isOutgoing { @@ -96,16 +37,61 @@ struct ChatBubbleView: View { VStack { if !message.attachments.isEmpty { - AsyncImage(url: message.attachments.first!.full) { image in - image.resizable() - .scaledToFill() - //.aspectRatio(1.5, contentMode: .fill) - //.clipped() - } placeholder: { - ProgressView() + if message.attachments.count == 1 { + if message.attachments.first!.type == .image || message.attachments.first!.type == .gif { + let result = imageDimensions(url: message.attachments.first!.full.absoluteString) + if message.attachments.first!.type != .gif { + AsyncImage(url: message.attachments.first!.full) { image in + image.resizable() + .interpolation(.low) + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: 4)) + } placeholder: { + ProgressView() + } + .frame( + height: result.0 > result.1 + ? result.1 / (result.0 / (geometryProxy.size.width - 80)) + : UIScreen.main.bounds.height > UIScreen.main.bounds.width ? UIScreen.main.bounds.height / 2.5 : UIScreen.main.bounds.width / 2.5 + ) + } else { + if result.0 < result.1 { + GifImageView(message.attachments.first!.full) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .frame( + width: result.1 > (UIScreen.main.bounds.height > UIScreen.main.bounds.width ? UIScreen.main.bounds.height / 2.5 : UIScreen.main.bounds.width / 2.5) + ? result.0 / (result.1 / (UIScreen.main.bounds.height > UIScreen.main.bounds.width ? UIScreen.main.bounds.height / 2.5 : UIScreen.main.bounds.width / 2.5)) + : result.0, + height: result.1 > (UIScreen.main.bounds.height > UIScreen.main.bounds.width ? UIScreen.main.bounds.height / 2.5 : UIScreen.main.bounds.width / 2.5) + ? UIScreen.main.bounds.height > UIScreen.main.bounds.width ? UIScreen.main.bounds.height / 2.5 : UIScreen.main.bounds.width / 2.5 + : result.1 + ) + } else { + GifImageView(message.attachments.first!.full) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .frame( + height: result.1 / (result.0 / (geometryProxy.size.width - 80)) + ) + } + } + } else { + let result = imageDimensions(url: message.attachments.first!.full.absoluteString) + AsyncImage(url: message.attachments.first!.full) { image in + image.resizable() + .interpolation(.low) + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: 4)) + } placeholder: { + ProgressView() + } + .frame( + height: result.0 > result.1 + ? result.1 / (result.0 / (geometryProxy.size.width - 80)) + : UIScreen.main.bounds.height > UIScreen.main.bounds.width ? UIScreen.main.bounds.height / 2.5 : UIScreen.main.bounds.width / 2.5 + ) + } + } else { } - .frame(maxHeight: 400) - //.frame(width: 50, height: 50) } if !message.text.isEmpty { @@ -125,11 +111,58 @@ struct ChatBubbleView: View { .padding(.leading, message.isOutgoing ? 40 : 0) .padding(.trailing, !message.isOutgoing ? 40 : 0) } - } + } + + func imageDimensions(url: String) -> (CGFloat, CGFloat) { + if let imageSource = CGImageSourceCreateWithURL(URL(string: url)! as CFURL, nil) { + if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? { + let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat + let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat + return (pixelWidth ?? 0, pixelHeight ?? 0) + } + } + return (0, 0) + } +} + +enum URLType { + case name(String) // local file name of gif + case url(URL) // remote url + + var url: URL? { + switch self { + case .name(let name): + return Bundle.main.url(forResource: name, withExtension: "gif") + case .url(let remoteURL): + return remoteURL + } + } +} + +struct GifImageView: UIViewRepresentable { + private let name: URL + init(_ name: URL) { + self.name = name + } + + func makeUIView(context: Context) -> WKWebView { + let webview = WKWebView() + let url = name + let data = try? Data(contentsOf: url) + if data != nil { + webview.load(data!, mimeType: "image/gif", characterEncodingName: "UTF-8", baseURL: url.deletingLastPathComponent()) + webview.scrollView.isScrollEnabled = false + } + return webview + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + uiView.reload() + } } /* -#Preview { - ChatBubbleView(conversationViewModel: ConversationViewModel(), index: 0) -} -*/ + #Preview { + ChatBubbleView(conversationViewModel: ConversationViewModel(), index: 0) + } + */ diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 46dba158c..c95ecddde 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -152,7 +152,15 @@ struct ConversationFragment: View { if #available(iOS 16.0, *) { ZStack(alignment: .bottomTrailing) { - list + UIList(viewModel: viewModel, + paginationState: paginationState, + conversationViewModel: conversationViewModel, + isScrolledToBottom: $isScrolledToBottom, + showMessageMenuOnLongPress: showMessageMenuOnLongPress, + geometryProxy: geometry, + sections: conversationViewModel.conversationMessagesSection, + ids: conversationViewModel.conversationMessagesIds + ) if !isScrolledToBottom { Button { @@ -209,6 +217,7 @@ struct ConversationFragment: View { conversationViewModel.resetMessage() } } else { + /* ScrollViewReader { proxy in List { ForEach(0.., isScrolledToTop: Binding, showMessageMenuOnLongPress: Bool, sections: [MessagesSection], ids: [String]) { + init(conversationViewModel: ConversationViewModel, viewModel: ChatViewModel, paginationState: PaginationState, isScrolledToBottom: Binding, isScrolledToTop: Binding, showMessageMenuOnLongPress: Bool, geometryProxy: GeometryProxy, sections: [MessagesSection], ids: [String]) { self.conversationViewModel = conversationViewModel self.viewModel = viewModel self.paginationState = paginationState self._isScrolledToBottom = isScrolledToBottom self._isScrolledToTop = isScrolledToTop self.showMessageMenuOnLongPress = showMessageMenuOnLongPress + self.geometryProxy = geometryProxy self.sections = sections self.ids = ids } @@ -373,7 +377,7 @@ struct UIList: UIViewRepresentable { let row = sections[indexPath.section].rows[indexPath.row] if #available(iOS 16.0, *) { tableViewCell.contentConfiguration = UIHostingConfiguration { - ChatBubbleView(conversationViewModel: conversationViewModel, message: row) + ChatBubbleView(conversationViewModel: conversationViewModel, message: row, geometryProxy: geometryProxy) .padding(.vertical, 1) .padding(.horizontal, 10) .onTapGesture { } diff --git a/Linphone/UI/Main/Conversations/Model/Attachment.swift b/Linphone/UI/Main/Conversations/Model/Attachment.swift index e9973cbaf..39d456e79 100644 --- a/Linphone/UI/Main/Conversations/Model/Attachment.swift +++ b/Linphone/UI/Main/Conversations/Model/Attachment.swift @@ -22,6 +22,7 @@ import Foundation public enum AttachmentType: String, Codable { case image case video + case gif public var title: String { switch self { diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index b711fd279..059cb9d10 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -99,16 +99,6 @@ class ConversationViewModel: ObservableObject { if self.displayedConversation != nil { let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: self.conversationMessagesList.count, end: self.conversationMessagesList.count + 30) - //For List - /* - historyEvents.reversed().forEach { eventLog in - DispatchQueue.main.async { - self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog)) - } - } - */ - - //For ScrollView var conversationMessage: [Message] = [] historyEvents.enumerated().forEach { index, eventLog in DispatchQueue.main.async { @@ -126,7 +116,7 @@ class ConversationViewModel: ObservableObject { if content.filePath == nil || content.filePath!.isEmpty { self.downloadContent(chatMessage: eventLog.chatMessage!, content: content) } else { - let attachment = Attachment(id: UUID().uuidString, url: URL(string: "file://" + content.filePath!)!, type: .image) + let attachment = Attachment(id: UUID().uuidString, url: URL(string: self.getNewFilePath(name: content.name ?? ""))!, type: (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image) attachmentList.append(attachment) } } @@ -350,18 +340,8 @@ class ConversationViewModel: ObservableObject { } } - func generateThumbnail(path: URL) -> UIImage? { - do { - let asset = AVURLAsset(url: path, options: nil) - let imgGenerator = AVAssetImageGenerator(asset: asset) - imgGenerator.appliesPreferredTrackTransform = true - let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) - let thumbnail = UIImage(cgImage: cgImage) - return thumbnail - } catch let error { - print("*** Error generating thumbnail: \(error.localizedDescription)") - return nil - } + func getNewFilePath(name: String) -> String { + return "file://" + Factory.Instance.getDownloadDir(context: nil) + name } } struct LinphoneCustomEventLog: Hashable {