From caf9a510e0e3ad5f96fbbbeddcca3f588c90dd9f Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 16 Jan 2023 11:58:29 +0100 Subject: [PATCH] Add Upload media (Photos, Videos, Files) feature --- Classes/LinphoneManager.m | 19 +- .../Views/ChatConversationViewSwift.swift | 213 +++++++++++++----- Classes/Swift/Chat/Views/MessageView.swift | 13 +- Classes/Utils/FileTransferDelegate.h | 1 + Classes/Utils/FileTransferDelegate.m | 55 +++++ Classes/linphone-Bridging-Header.h | 1 + 6 files changed, 242 insertions(+), 60 deletions(-) diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index 4192c4e2d..d9b72f860 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -914,9 +914,9 @@ static void linphone_iphone_popup_password_request(LinphoneCore *lc, LinphoneAut return; if (hasFile) { - if (PhoneMainView.instance.currentView == ChatConversationView.compositeViewDescription && room == PhoneMainView.instance.currentRoom) + if (PhoneMainView.instance.currentView == ChatConversationViewSwift.compositeViewDescription && room == PhoneMainView.instance.currentRoom) return; - [ChatConversationView autoDownload:msg]; + [self autoDownload:msg]; } // Post event @@ -930,6 +930,21 @@ static void linphone_iphone_popup_password_request(LinphoneCore *lc, LinphoneAut [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneMessageReceived object:self userInfo:dict]; } +- (void)autoDownload:(LinphoneChatMessage *)message { + LinphoneContent *content = linphone_chat_message_get_file_transfer_information(message); + NSString *name = [NSString stringWithUTF8String:linphone_content_get_name(content)]; + NSString *fileType = [NSString stringWithUTF8String:linphone_content_get_type(content)]; + NSString *key = [ChatConversationViewSwift getKeyFromFileType:fileType fileName:name]; + + [LinphoneManager setValueInMessageAppData:name forKey:key inMessage:message]; + dispatch_async(dispatch_get_main_queue(), ^{ + [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneMessageReceived object:VIEW(ChatConversationViewSwift)]; + if (![VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId] && [ConfigManager.instance lpConfigBoolForKeyWithKey:@"auto_write_to_gallery_preference"]) { + [ChatConversationViewSwift writeMediaToGalleryFromName:name fileType:fileType]; + } + }); +} + static void linphone_iphone_message_received(LinphoneCore *lc, LinphoneChatRoom *room, LinphoneChatMessage *message) { [(__bridge LinphoneManager *)linphone_core_cbs_get_user_data(linphone_core_get_current_callbacks(lc)) onMessageReceived:lc room:room message:message]; } diff --git a/Classes/Swift/Chat/Views/ChatConversationViewSwift.swift b/Classes/Swift/Chat/Views/ChatConversationViewSwift.swift index 03825b24b..4d827003d 100644 --- a/Classes/Swift/Chat/Views/ChatConversationViewSwift.swift +++ b/Classes/Swift/Chat/Views/ChatConversationViewSwift.swift @@ -57,7 +57,7 @@ import AVFoundation var mediaSelectorVisible = false var mediaCollectionView : [UIImage] = [] - var mediaNameCollection : [String] = [] + var mediaURLCollection : [URL] = [] var collectionView: UICollectionView = { let top_bar_height = 66.0 @@ -121,6 +121,9 @@ import AVFoundation return menu }() + var fileContext : [Data] = [] + + override func viewDidLoad() { super.viewDidLoad( backAction: { @@ -187,9 +190,10 @@ import AVFoundation self.isComposingView.transform = CGAffineTransformIdentity; self.mediaSelector.transform = CGAffineTransformIdentity; self.contentView.transform = CGAffineTransformIdentity; - //self.collectionView = nil self.mediaCollectionView = [] - self.mediaNameCollection = [] + self.mediaURLCollection = [] + self.fileContext = [] + self.messageView.fileContext = false } @@ -584,6 +588,7 @@ import AVFoundation func sendMessageInMessageField(rootMessage: ChatMessage?) { if sendMessage(message: messageView.messageText.text, withExterlBodyUrl: nil, rootMessage: rootMessage) { messageView.messageText.text = "" + messageView.isComposing = false } } @@ -601,33 +606,77 @@ import AVFoundation stopVoiceRecordPlayer() linphone_chat_message_add_content(rootMessage, voiceContent) } - *//* - if fileContext.count() > 0 { - if linphone_chat_room_get_capabilities(chatRoom?.getCobject) & LinphoneChatRoomCapabilitiesConference != 0 { - startMultiFilesUpload(rootMessage) - } else { - var i = 0 - for i in 0..<(fileContext.count() - 1) { - startUploadData(fileContext.datasArray[i], withType: fileContext.typesArray[i], withName: fileContext.namesArray[i], andMessage: nil, rootMessage: nil) - } - if isOneToOne { - startUploadData(fileContext.datasArray[i], withType: fileContext.typesArray[i], withName: fileContext.namesArray[i], andMessage: nil, rootMessage: nil) - if messageView.messageText.text != "" { - sendMessage(message: messageView.messageText.text, withExterlBodyUrl: nil, rootMessage: rootMessage) - } + */ + if fileContext.count > 0 { + let conference = chatRoom!.hasCapability(mask: Int(LinphoneChatRoomCapabilitiesConference.rawValue)) + if (linphone_chat_room_get_capabilities(chatRoom?.getCobject) != 0) && conference { + let result = ChatMessage.getSwiftObject(cObject: rootMessage!) + startMultiFilesUpload(result) } else { - startUploadData(fileContext.datasArray[i], withType: fileContext.typesArray[i], withName: fileContext.namesArray[i], andMessage: messageField.text(), rootMessage: rootMessage) + for i in 0..<(fileContext.count) { + startUploadData(fileContext[i], withType: FileType.init(mediaURLCollection[i].pathExtension)?.getGroupTypeFromFile(), withName: mediaURLCollection[i].lastPathComponent, andMessage: nil, rootMessage: nil) + } + if messageView.messageText.text != "" { + let result = ChatMessage.getSwiftObject(cObject: rootMessage!) + sendMessageInMessageField(rootMessage: result) + } } - } - - clearMessageView() - return - } - */ + + fileContext = [] + messageView.fileContext = false + mediaSelectorVisible = false + mediaSelectorHeight = 0.0 + self.isComposingView.transform = CGAffineTransformIdentity; + self.mediaSelector.transform = CGAffineTransformIdentity; + self.contentView.transform = CGAffineTransformIdentity; + self.mediaCollectionView = [] + self.mediaURLCollection = [] + return + } + let result = ChatMessage.getSwiftObject(cObject: rootMessage!) sendMessageInMessageField(rootMessage: result) } + func startMultiFilesUpload(_ rootMessage: ChatMessage?) -> Bool { + let fileTransfer = FileTransferDelegate() + fileTransfer.text = messageView.messageText.text + fileTransfer.uploadFileContent(forSwift: fileContext, urlList: mediaURLCollection, for: chatRoom?.getCobject, rootMessage: rootMessage?.getCobject) + messageView.messageText.text = "" + tableController.scroll(toBottom: true) + return true + } + + @objc class func writeFileInImagesDirectory(_ data: Data?, name: String?) { + let filePath = URL(fileURLWithPath: LinphoneManager.imagesDirectory()).appendingPathComponent(name ?? "").path + if name != nil || (name == "") { + print("try to write file in \(filePath)") + } + FileManager.default.createFile( + atPath: filePath, + contents: data, + attributes: nil) + } + + func startUploadData(_ data: Data?, withType type: String?, withName name: String?, andMessage message: String?, rootMessage: ChatMessage?) -> Bool { + let fileTransfer = FileTransferDelegate.init() + if let message { + fileTransfer.text = message + } + var resultType = "file" + var key = "localfile" + if type == "file_video_default" { + resultType = "video" + key = "localvideo" + } else if type == "file_picture_default" { + resultType = "image" + key = "localimage" + } + fileTransfer.uploadData(data, for: chatRoom?.getCobject, type: resultType, subtype: resultType, name: name, key: key, rootMessage: rootMessage?.getCobject) + tableController.scroll(toBottom: true) + return true + } + func on_chat_room_chat_message_received(_ cr: ChatRoom?, _ event_log: EventLog?) { let chat = event_log?.chatMessage if chat == nil { @@ -780,21 +829,16 @@ import AVFoundation }) } - class func autoDownload(_ message: ChatMessage?) { - let content = linphone_chat_message_get_file_transfer_information(message?.getCobject) - let name = String(utf8String: linphone_content_get_name(content)) - let fileType = String(utf8String: linphone_content_get_type(content)) - let key = ChatConversationView.getKeyFromFileType(fileType, fileName: name) - - LinphoneManager.setValueInMessageAppData(name, forKey: key, in: message?.getCobject) - DispatchQueue.main.async(execute: { - if !VFSUtil.vfsEnabled(groupName: kLinphoneMsgNotificationAppGroupId) && ConfigManager.instance().lpConfigBoolForKey(key: "auto_write_to_gallery_preference") { - ChatConversationViewSwift.writeMediaToGallery(name: name, fileType: fileType) - } - }) + @objc class func getKeyFromFileType(_ fileType: String?, fileName name: String?) -> String? { + if fileType == "video" { + return "localvideo" + } else if (fileType == "image") || name?.hasSuffix("JPG") ?? false || name?.hasSuffix("PNG") ?? false || name?.hasSuffix("jpg") ?? false || name?.hasSuffix("png") ?? false { + return "localimage" + } + return "localfile" } - class func writeMediaToGallery(name: String?, fileType: String?) { + @objc class func writeMediaToGalleryFromName(_ name: String?, fileType: String?) { let filePath = LinphoneManager.validFilePath(name) let fileManager = FileManager.default if fileManager.fileExists(atPath: filePath!) { @@ -990,6 +1034,7 @@ import AVFoundation if(self.mediaCollectionView.count > 0 && !mediaSelectorVisible){ self.selectionMedia() self.messageView.sendButton.isEnabled = true + self.messageView.fileContext = true } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) @@ -999,10 +1044,12 @@ import AVFoundation let deleteButton = CallControlButton(width: 22, height: 22, buttonTheme:VoipTheme.nav_black_button("reply_cancel"), onClickAction: { self.collectionView.deleteItems(at: [indexPath]) self.mediaCollectionView.remove(at: indexPath.row) - self.mediaNameCollection.remove(at: indexPath.row) + self.mediaURLCollection.remove(at: indexPath.row) + self.fileContext.remove(at: indexPath.row) if(self.mediaCollectionView.count == 0){ + self.messageView.fileContext = false self.selectionMedia() - if self.messageView.messageText.text.isEmpty { + if self.messageView.messageText.text.isEmpty{ self.messageView.sendButton.isEnabled = false } else { self.messageView.sendButton.isEnabled = true @@ -1013,10 +1060,10 @@ import AVFoundation let imageCell = mediaCollectionView[indexPath.row] var myImageView = UIImageView() - if(mediaNameCollection[indexPath.row] == FileType.file_picture_default.rawValue || mediaNameCollection[indexPath.row] == FileType.file_video_default.rawValue){ + if(FileType.init(mediaURLCollection[indexPath.row].pathExtension)?.getGroupTypeFromFile() == FileType.file_picture_default.rawValue || FileType.init(mediaURLCollection[indexPath.row].pathExtension)?.getGroupTypeFromFile() == FileType.file_video_default.rawValue){ myImageView = UIImageView(image: imageCell) }else{ - let fileNameText = mediaNameCollection[indexPath.row] + let fileNameText = mediaURLCollection[indexPath.row].lastPathComponent let fileName = SwiftUtil.textToImage(drawText:fileNameText, inImage:imageCell, forReplyBubble:false) myImageView = UIImageView(image: fileName) } @@ -1025,7 +1072,7 @@ import AVFoundation viewCell.addSubview(myImageView) myImageView.alignParentBottom(withMargin: 4).alignParentLeft(withMargin: 4).done() - if(mediaNameCollection[indexPath.row] == FileType.file_video_default.rawValue){ + if(FileType.init(mediaURLCollection[indexPath.row].pathExtension)?.getGroupTypeFromFile() == FileType.file_video_default.rawValue){ var imagePlay = UIImage() if #available(iOS 13.0, *) { imagePlay = (UIImage(named: "vr_play")!.withTintColor(.white)) @@ -1053,14 +1100,24 @@ import AVFoundation for item in itemProviders { if item.canLoadObject(ofClass: UIImage.self) { item.loadObject(ofClass: UIImage.self) { (image, error) in - DispatchQueue.main.async { - if let image = image as? UIImage { - self.collectionView.performBatchUpdates({ - let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) - self.mediaCollectionView.append(image) - self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(FileType.file_picture_default.rawValue) - }, completion: nil) + if item.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + item.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { urlFile, err in + DispatchQueue.main.async { + if let image = image as? UIImage { + self.collectionView.performBatchUpdates({ + self.mediaURLCollection.append(urlFile!) + let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) + self.mediaCollectionView.append(image) + self.collectionView.insertItems(at: [indexPath]) + }, completion: nil) + } + } + do { + let data = try Data(contentsOf: urlFile!) + self.fileContext.append(data) + } catch let error{ + print(error.localizedDescription) + } } } } @@ -1074,7 +1131,13 @@ import AVFoundation let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image!) self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(FileType.file_video_default.rawValue) + self.mediaURLCollection.append(urlFile!) + do { + let data = try Data(contentsOf: url) + self.fileContext.append(data) + } catch let error{ + print(error.localizedDescription) + } }, completion: nil) } } @@ -1096,7 +1159,21 @@ import AVFoundation let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image) self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(FileType.file_picture_default.rawValue) + + let date = Date() + let df = DateFormatter() + df.dateFormat = "yyyy-MM-dd-HHmmss" + let dateString = df.string(from: date) + + let fileUrl = URL(string: dateString + ".jpeg") + + self.mediaURLCollection.append(fileUrl!) + + let data = image.jpegData(compressionQuality: 1) + self.fileContext.append(data!) + + + }, completion: nil) case "public.movie": let videoUrl = info[UIImagePickerController.InfoKey.mediaURL] as! URL @@ -1106,7 +1183,13 @@ import AVFoundation let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image!) self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(FileType.file_video_default.rawValue) + self.mediaURLCollection.append(videoUrl) + do { + let data = try Data(contentsOf: videoUrl) + self.fileContext.append(data) + } catch let error{ + print(error.localizedDescription) + } }, completion: nil) default: print("Mismatched type: \(mediaType)") @@ -1130,7 +1213,13 @@ import AVFoundation let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image!) self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(FileType.file_picture_default.rawValue) + self.mediaURLCollection.append(url) + do { + let data = try Data(contentsOf: url) + self.fileContext.append(data) + } catch let error{ + print(error.localizedDescription) + } }, completion: nil) }else if(videoExtension.contains(url.pathExtension.lowercased())){ let thumbnail = createThumbnailOfVideoFromFileURL(videoURL: url.relativeString) @@ -1138,7 +1227,13 @@ import AVFoundation let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(thumbnail!) self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(FileType.file_video_default.rawValue) + self.mediaURLCollection.append(url) + do { + let data = try Data(contentsOf: url) + self.fileContext.append(data) + } catch let error{ + print(error.localizedDescription) + } }, completion: nil) }else{ //let otherFile = UIImage(named: "chat_error") @@ -1148,7 +1243,13 @@ import AVFoundation let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(otherFileImage!) self.collectionView.insertItems(at: [indexPath]) - self.mediaNameCollection.append(url.lastPathComponent) + self.mediaURLCollection.append(url) + do { + let data = try Data(contentsOf: url) + self.fileContext.append(data) + } catch let error{ + print(error.localizedDescription) + } }, completion: nil) } } diff --git a/Classes/Swift/Chat/Views/MessageView.swift b/Classes/Swift/Chat/Views/MessageView.swift index fec103d9f..5b02f74a7 100644 --- a/Classes/Swift/Chat/Views/MessageView.swift +++ b/Classes/Swift/Chat/Views/MessageView.swift @@ -35,6 +35,8 @@ class MessageView: UIView, UITextViewDelegate { let messageTextView = UIView() let messageText = UITextView() let ephemeralIndicator = UIImageView(image: UIImage(named: "ephemeral_messages_color_A.png")) + var fileContext = false + var isComposing = false override init(frame: CGRect) { super.init(frame: frame) @@ -82,10 +84,17 @@ class MessageView: UIView, UITextViewDelegate { func textViewDidChangeSelection(_ textView: UITextView) { let chatRoom = ChatRoom.getSwiftObject(cObject: PhoneMainView.instance().currentRoom) - if messageText.text.isEmpty { + if messageText.text.isEmpty && fileContext == false { sendButton.isEnabled = false } else { - chatRoom.compose() + if !isComposing { + chatRoom.compose() + let timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { timer in + self.isComposing = false + } + } + isComposing = true + sendButton.isEnabled = true } } diff --git a/Classes/Utils/FileTransferDelegate.h b/Classes/Utils/FileTransferDelegate.h index 3fa22bb55..a3823f947 100644 --- a/Classes/Utils/FileTransferDelegate.h +++ b/Classes/Utils/FileTransferDelegate.h @@ -25,6 +25,7 @@ @interface FileTransferDelegate : NSObject - (void)uploadFileContent: (FileContext *)context forChatRoom:(LinphoneChatRoom *)chatRoom rootMessage:(LinphoneChatMessage *)rootMessage; +- (void)uploadFileContentForSwift: (NSArray*)context urlList:(NSArray*)urls forChatRoom:(LinphoneChatRoom *)chatRoom rootMessage:(LinphoneChatMessage *)rootMessage; - (void)uploadData:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom type:(NSString *)type subtype:(NSString *)subtype name:(NSString *)name key:(NSString *)key rootMessage:(LinphoneChatMessage *)rootMessage; - (void)uploadImage:(UIImage *)image forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality; - (void)uploadFile:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom withName:(NSString *)name rootMessage:(LinphoneChatMessage *)rootMessage; diff --git a/Classes/Utils/FileTransferDelegate.m b/Classes/Utils/FileTransferDelegate.m index 8f87b1408..b8f4e0148 100644 --- a/Classes/Utils/FileTransferDelegate.m +++ b/Classes/Utils/FileTransferDelegate.m @@ -184,6 +184,61 @@ static void file_transfer_progress_indication_send(LinphoneChatMessage *message, } } +- (void)uploadFileContentForSwift: (NSArray*)context urlList:(NSArray*)urls forChatRoom:(LinphoneChatRoom *)chatRoom rootMessage:(LinphoneChatMessage *)rootMessage{ + [LinphoneManager.instance.fileTransferDelegates addObject:self]; + + _message = rootMessage; + NSMutableArray *names = [[NSMutableArray alloc] init]; + NSMutableArray *types = [[NSMutableArray alloc] init]; + + int i = 0; + for (i = 0; i < [context count]; ++i) { + LinphoneContent *content = linphone_core_create_content(linphone_chat_room_get_core(chatRoom)); + //NSString *type = @"image";//[urls objectAtIndex:i].pathExtension; + + NSString *type = [self getTypeForFileTransferDelegate:[urls objectAtIndex:i].pathExtension.localizedLowercaseString]; + NSString *name = [urls objectAtIndex:i].lastPathComponent; + NSData *data = [context objectAtIndex:i]; + [ChatConversationViewSwift writeFileInImagesDirectory:data name:name]; + + linphone_content_set_type(content, [type UTF8String]); + linphone_content_set_subtype(content, [name.pathExtension UTF8String]); + linphone_content_set_name(content, [name UTF8String]); + linphone_content_set_file_path(content, [[LinphoneManager imagesDirectory] stringByAppendingPathComponent:name].UTF8String); + [names addObject:name]; + [types addObject:type]; + linphone_chat_message_add_file_content(_message, content); + linphone_content_unref(content); + } + + BOOL basic = linphone_chat_message_get_chat_room(rootMessage); + const LinphoneAccountParams *params = linphone_account_get_params(linphone_core_get_default_account(LC)); + BOOL cpimEnabled = linphone_account_params_cpim_in_basic_chat_room_enabled(params); + + if ((!basic || cpimEnabled) && _text!=nil && ![_text isEqualToString:@""]) + linphone_chat_message_add_utf8_text_content(_message, [_text UTF8String]); + + // todo indication progress + [LinphoneManager setValueInMessageAppData:names forKey:@"multiparts" inMessage:_message]; + [LinphoneManager setValueInMessageAppData:types forKey:@"multipartstypes" inMessage:_message]; + + LOGI(@"%p Uploading content from message %p", self, _message); + linphone_chat_message_send(_message); + if (basic && !cpimEnabled && _text!=nil && ![_text isEqualToString:@""]) { + linphone_chat_message_send(linphone_chat_room_create_message_from_utf8(linphone_chat_message_get_chat_room(rootMessage), _text.UTF8String)); + } +} + +- (NSString*)getTypeForFileTransferDelegate:(NSString *)type { + if([type isEqual:@"png"] || [type isEqual:@"jpg"] || [type isEqual:@"jpeg"] || [type isEqual:@"bmp"] || [type isEqual:@"heic"] || [type isEqual:@"file_picture_default"]){ + return @"image"; + }else if([type isEqual:@"mkv"] || [type isEqual:@"avi"] || [type isEqual:@"mov"] || [type isEqual:@"mp4"] || [type isEqual:@"file_video_default"]){ + return @"video"; + }else{ + return @"file"; + } +} + - (void)uploadImage:(UIImage *)image forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality { NSString *name = [NSString stringWithFormat:@"%li-%f.jpg", (long)image.hash, [NSDate timeIntervalSinceReferenceDate]]; diff --git a/Classes/linphone-Bridging-Header.h b/Classes/linphone-Bridging-Header.h index 333a75d2f..7da4b8a16 100644 --- a/Classes/linphone-Bridging-Header.h +++ b/Classes/linphone-Bridging-Header.h @@ -17,3 +17,4 @@ #import "UIChatBubbleTextCell.h" #import "ChatConversationTableView.h" #import "EphemeralSettingsView.h" +#import "FileTransferDelegate.h"