From b66c3ad9164078721afcf5a01820e4f1bc9e1e6d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 23 Jul 2021 12:06:05 +0200 Subject: [PATCH] Audio chat messages --- Classes/AppManager.swift | 12 + Classes/Base.lproj/ChatConversationView.xib | 103 ++++- Classes/CallManager.swift | 19 + Classes/ChatConversationTableView.h | 6 +- Classes/ChatConversationView.h | 26 ++ Classes/ChatConversationView.m | 402 ++++++++++++++++-- Classes/FirstLoginView.m | 2 +- Classes/LinphoneManager.h | 2 + Classes/LinphoneManager.m | 4 +- .../Base.lproj/UIChatBubblePhotoCell.xib | 53 ++- Classes/LinphoneUI/UIChatBubblePhotoCell.h | 10 + Classes/LinphoneUI/UIChatBubblePhotoCell.m | 133 +++++- Classes/LinphoneUI/UIChatBubbleTextCell.h | 4 + Classes/LinphoneUI/UIChatBubbleTextCell.m | 125 +++++- Classes/PhoneMainView.m | 2 +- Classes/Utils/FileTransferDelegate.h | 4 +- Classes/Utils/FileTransferDelegate.m | 15 +- Classes/Utils/Utils.h | 2 +- Classes/Utils/Utils.m | 6 +- Resources/en.lproj/Localizable.strings | Bin 52514 -> 52874 bytes Resources/fr.lproj/Localizable.strings | Bin 57096 -> 57484 bytes Resources/images/vr_off.png | Bin 0 -> 12846 bytes Resources/images/vr_on.png | Bin 0 -> 15718 bytes Resources/images/vr_pause.png | Bin 0 -> 6392 bytes Resources/images/vr_play.png | Bin 0 -> 7003 bytes Resources/images/vr_stop.png | Bin 0 -> 5528 bytes Resources/images/vr_wave.png | Bin 0 -> 15224 bytes linphone.xcodeproj/project.pbxproj | 36 +- 28 files changed, 868 insertions(+), 98 deletions(-) create mode 100644 Resources/images/vr_off.png create mode 100644 Resources/images/vr_on.png create mode 100644 Resources/images/vr_pause.png create mode 100644 Resources/images/vr_play.png create mode 100644 Resources/images/vr_stop.png create mode 100644 Resources/images/vr_wave.png diff --git a/Classes/AppManager.swift b/Classes/AppManager.swift index d1987e746..9a86a02e9 100644 --- a/Classes/AppManager.swift +++ b/Classes/AppManager.swift @@ -64,4 +64,16 @@ enum NetworkType: Int { //The recording prefix is used to identify recordings in the cache directory. //We will use name_dayName-day-monthName-year to separate recordings by days, then hour-minutes-seconds to order them in each day. } + + @objc static func removeFile(file: String) { + let fileManager = FileManager.default + do { + try fileManager.removeItem(atPath: file) + Log.directLog(BCTBX_LOG_MESSAGE, text: "File :\(file) removed") + + } catch { + print("Could not remove file : \(file) \(error)") + } + } + } diff --git a/Classes/Base.lproj/ChatConversationView.xib b/Classes/Base.lproj/ChatConversationView.xib index e321384a1..62255dabe 100644 --- a/Classes/Base.lproj/ChatConversationView.xib +++ b/Classes/Base.lproj/ChatConversationView.xib @@ -34,9 +34,18 @@ + + + + + + + + + @@ -209,7 +218,7 @@ - + @@ -221,7 +230,7 @@ + @@ -551,8 +625,21 @@ + - + @@ -637,10 +724,14 @@ - + + + + + diff --git a/Classes/CallManager.swift b/Classes/CallManager.swift index 68c0869eb..a9546176a 100644 --- a/Classes/CallManager.swift +++ b/Classes/CallManager.swift @@ -639,6 +639,25 @@ import AVFoundation AnyHashable("message"): message ]) } + + @objc func activateAudioSession() { + lc?.activateAudioSession(actived: true) + } + + @objc func getSpeakerSoundCard() -> String? { + var speakerCard: String? = nil + var earpieceCard: String? = nil + lc?.audioDevices.forEach { device in + if (device.hasCapability(capability: .CapabilityPlay)) { + if (device.type == .Speaker) { + speakerCard = device.id + } else if (device.type == .Earpiece) { + earpieceCard = device.id + } + } + } + return speakerCard != nil ? speakerCard : earpieceCard + } } diff --git a/Classes/ChatConversationTableView.h b/Classes/ChatConversationTableView.h index 3fb5e83be..1a3f783d7 100644 --- a/Classes/ChatConversationTableView.h +++ b/Classes/ChatConversationTableView.h @@ -39,10 +39,10 @@ @protocol ChatConversationDelegate -- (BOOL)resendMultiFiles:(FileContext *)newFileContext message:(NSString *)message; -- (BOOL)resendFile:(NSData *)data withName:(NSString *)name type:(NSString *)type key:(NSString *)key message:(NSString *)message; +- (BOOL)resendMultiFiles:(FileContext *)newFileContext message:(NSString *)message voiceContent:(LinphoneContent *)voiceContent; +- (BOOL)resendFile:(NSData *)data withName:(NSString *)name type:(NSString *)type key:(NSString *)key message:(NSString *)message voiceContent:(LinphoneContent *)voiceContent; - (BOOL)startFileUpload:(NSData *)data withName:(NSString *)name; -- (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url; +- (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url voiceContent:(LinphoneContent *)voiceContent; - (void)tableViewIsScrolling; @end diff --git a/Classes/ChatConversationView.h b/Classes/ChatConversationView.h index 06bb89c17..1a8d77309 100644 --- a/Classes/ChatConversationView.h +++ b/Classes/ChatConversationView.h @@ -92,6 +92,28 @@ @property (weak, nonatomic) IBOutlet UIInterfaceStyleButton *toggleMenuButton; @property (weak, nonatomic) IBOutlet UIImageView *ephemeralndicator; + +// Voice recording +@property (strong, nonatomic) IBOutlet UIView *vrView; +@property (weak, nonatomic) IBOutlet UIView *vrInnerView; +@property (weak, nonatomic) IBOutlet UIButton *vrDeleteButton; +@property (weak, nonatomic) IBOutlet UIButton *vrPlayButton; +@property (weak, nonatomic) IBOutlet UIImageView *vrWave; +@property (weak, nonatomic) IBOutlet UIView *vrWaveMask; +@property (weak, nonatomic) IBOutlet UIView *vrWaveMaskPlayer; +@property (weak, nonatomic) IBOutlet UILabel *vrDurationLabel; +@property NSTimer *vrRecordTimer; +@property NSTimer *vrPlayerTimer; +@property (weak, nonatomic) IBOutlet UIButton *toggleRecord; +@property BOOL isVoiceRecording; +@property BOOL isPendingVoiceRecord; +@property BOOL isPlayingVoiceRecording; +@property LinphoneRecorder *voiceRecorder; +@property LinphonePlayer *sharedVoicePlayer; +@property BOOL showVoiceRecorderView; +@property BOOL preservePendingRecording; + + + (void)markAsRead:(LinphoneChatRoom *)chatRoom; + (void)autoDownload:(LinphoneChatMessage *)message; +(NSString *)getKeyFromFileType:(NSString *)fileType fileName:(NSString *)name; @@ -123,4 +145,8 @@ - (NSURL *)getICloudFileUrl:(NSString *)name; - (void)removeCallBacks; +-(void) startSharedPlayer:(const char *)path; +-(void) stopSharedPlayer; +-(BOOL) sharedPlayedIsPlaying:(const char *)path; + @end diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index bfdca00f3..05ec3b14c 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -120,6 +120,7 @@ [NSNumber numberWithFloat:0.5], NSLocalizedString(@"Average", nil), [NSNumber numberWithFloat:0.0], NSLocalizedString(@"Minimum", nil), nil]; composingVisible = false; + [self initSharedPlayer]; } return self; } @@ -187,6 +188,12 @@ static UICompositeViewDescription *compositeDescription = nil; [_imagesCollectionView registerClass:[UIImageViewDeletable class] forCellWithReuseIdentifier:NSStringFromClass([UIImageViewDeletable class])]; [_imagesCollectionView setDataSource:self]; [_toggleSelectionButton setImage:[UIImage imageNamed:@"select_all_default.png"] forState:UIControlStateSelected]; + + _vrInnerView.layer.cornerRadius = 5.0f; + _vrInnerView.layer.masksToBounds = YES; + _vrWaveMaskPlayer.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"color_L"]]; // rgba(1,88,7,0.2); + _showVoiceRecorderView = false; + } - (void)refreshData { @@ -202,6 +209,10 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillEnterBackground) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification @@ -222,6 +233,16 @@ static UICompositeViewDescription *compositeDescription = nil; selector:@selector(onLinphoneCoreReady:) name:kLinphoneGlobalStateUpdate object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(endVoicePlayingIfDoingSO:) + name:kLinphoneVoiceMessagePlayerLostFocus + object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(endVoicePlayingIfDoingSO:) + name:kLinphoneVoiceMessagePlayerEOF + object:nil]; if ([_fileContext count] > 0) { [UIView animateWithDuration:0 delay:0 @@ -236,6 +257,7 @@ static UICompositeViewDescription *compositeDescription = nil; CGRect tableViewFrame = [_tableController.tableView frame]; tableViewFrame.size.height -= 100; [_tableController.tableView setFrame:tableViewFrame]; + [self updateFramesInclRecordingView]; } completion:nil]; } @@ -245,10 +267,20 @@ static UICompositeViewDescription *compositeDescription = nil; CGRect popupFrame = _popupMenu.frame; popupFrame.size.height = 44 * [_popupMenu numberOfRowsInSection:0]; _popupMenu.frame = popupFrame; + + // Voice recording + _vrView.hidden = true; + _preservePendingRecording = false; } - (void)viewWillDisappear:(BOOL)animated { + + if (!_preservePendingRecording) + [self cancelVoiceRecording]; + else if (_isVoiceRecording) + [self stopVoiceRecording]; + [super viewWillDisappear:animated]; [self removeCallBacks]; @@ -256,7 +288,9 @@ static UICompositeViewDescription *compositeDescription = nil; [_messageField resignFirstResponder]; [self setComposingVisible:false withDelay:0]; // will hide the "user is composing.." message - + + [self stopAllPlays]; + [NSNotificationCenter.defaultCenter removeObserver:self]; PhoneMainView.instance.currentRoom = NULL; } @@ -287,10 +321,20 @@ static UICompositeViewDescription *compositeDescription = nil; _backButton.hidden = _tableController.isEditing; [_tableController scrollToBottom:true]; [self refreshImageDrawer]; + [self stopAllPlays]; + } #pragma mark - +- (void)applicationWillEnterBackground{ + if (!_preservePendingRecording) + [self cancelVoiceRecording]; + else if (_isVoiceRecording) + [self stopVoiceRecording]; +} + + - (void)configureForRoom:(BOOL)editing { if (!_chatRoom) { _chatView.hidden = YES; @@ -456,16 +500,24 @@ static UICompositeViewDescription *compositeDescription = nil; } } -- (BOOL)sendMessage:(NSString *)message withExterlBodyUrl:(NSURL *)externalUrl { +- (BOOL)sendMessage:(NSString *)message withExterlBodyUrl:(NSURL *)externalUrl andVoiceContent:(LinphoneContent *)voiceContent { if (_chatRoom == NULL) { LOGW(@"Cannot send message: No chatroom"); return FALSE; } - LinphoneChatMessage *msg = linphone_chat_room_create_message(_chatRoom, [message UTF8String]); + LinphoneChatMessage *msg = linphone_chat_room_create_empty_message(_chatRoom); + if (message && message.length > 0) + linphone_chat_message_add_utf8_text_content(msg, message.UTF8String); + if (externalUrl) { linphone_chat_message_set_external_body_url(msg, [[externalUrl absoluteString] UTF8String]); } + + // Voice recording + + if (voiceContent) + linphone_chat_message_add_content(msg, voiceContent); // we must ref & unref message because in case of error, it will be destroy otherwise linphone_chat_message_send(msg); @@ -512,10 +564,10 @@ static UICompositeViewDescription *compositeDescription = nil; [sheet addButtonWithTitle:NSLocalizedString(@"Send to this friend", nil) block:^() { if (![[self.messageField text] isEqualToString:@""]) { - [self sendMessageInMessageField]; + [self sendMessageInMessageFieldWithVoiceContent:nil]; } if (url) - [self sendMessage:url withExterlBodyUrl:nil]; + [self sendMessage:url withExterlBodyUrl:nil andVoiceContent:nil]; else [self startFileUpload:data withName:fileName]; }]; @@ -600,8 +652,8 @@ static UICompositeViewDescription *compositeDescription = nil; _addressLabel.frame = frame; } -- (void)sendMessageInMessageField { - if ([self sendMessage:[_messageField text] withExterlBodyUrl:nil]) { +- (void)sendMessageInMessageFieldWithVoiceContent:(LinphoneContent *)voiceContent { + if ([self sendMessage:[_messageField text] withExterlBodyUrl:nil andVoiceContent:voiceContent]) { scrollOnGrowingEnabled = FALSE; [_messageField setText:@""]; scrollOnGrowingEnabled = TRUE; @@ -657,6 +709,7 @@ static UICompositeViewDescription *compositeDescription = nil; CGRect tableRect = [_tableController.view frame]; tableRect.size.height -= diff; [_tableController.view setFrame:tableRect]; + [self updateFramesInclRecordingView]; // if we're showing the compose message, update it position if (![_composeLabel isHidden]) { @@ -681,28 +734,40 @@ static UICompositeViewDescription *compositeDescription = nil; } - (IBAction)onSendClick:(id)event { + LinphoneContent *voiceContent = nil; + if (_isPendingVoiceRecord && _voiceRecorder && linphone_recorder_get_file(_voiceRecorder)) { + voiceContent = linphone_recorder_create_content(_voiceRecorder); + _isPendingVoiceRecord = false; + [self cancelVoiceRecording]; + [self stopVoiceRecordPlayer]; + } + + if (!linphone_core_is_network_reachable(LC)) { + [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView:@"send a message"] animated:YES completion:nil]; + //return; + } if ([_fileContext count] > 0) { if (linphone_chat_room_get_capabilities(_chatRoom) & LinphoneChatRoomCapabilitiesConference) { - [self startMultiFilesUpload]; + [self startMultiFilesUploadWithVoiceContent:voiceContent]; } else { int i = 0; for (i = 0; i < [_fileContext count]-1; ++i) { - [self startUploadData:[_fileContext.datasArray objectAtIndex:i] withType:[_fileContext.typesArray objectAtIndex:i] withName:[_fileContext.namesArray objectAtIndex:i] andMessage:NULL]; + [self startUploadData:[_fileContext.datasArray objectAtIndex:i] withType:[_fileContext.typesArray objectAtIndex:i] withName:[_fileContext.namesArray objectAtIndex:i] andMessage:NULL voiceContent:voiceContent]; } if (isOneToOne) { - [self startUploadData:[_fileContext.datasArray objectAtIndex:i] withType:[_fileContext.typesArray objectAtIndex:i] withName:[_fileContext.namesArray objectAtIndex:i] andMessage:NULL]; + [self startUploadData:[_fileContext.datasArray objectAtIndex:i] withType:[_fileContext.typesArray objectAtIndex:i] withName:[_fileContext.namesArray objectAtIndex:i] andMessage:NULL voiceContent:voiceContent]; if (![[self.messageField text] isEqualToString:@""]) { - [self sendMessage:[_messageField text] withExterlBodyUrl:nil]; + [self sendMessage:[_messageField text] withExterlBodyUrl:nil andVoiceContent:voiceContent]; } } else { - [self startUploadData:[_fileContext.datasArray objectAtIndex:i] withType:[_fileContext.typesArray objectAtIndex:i] withName:[_fileContext.namesArray objectAtIndex:i] andMessage:[self.messageField text]]; + [self startUploadData:[_fileContext.datasArray objectAtIndex:i] withType:[_fileContext.typesArray objectAtIndex:i] withName:[_fileContext.namesArray objectAtIndex:i] andMessage:[self.messageField text] voiceContent:voiceContent]; } } [self clearMessageView]; return; } - [self sendMessageInMessageField]; + [self sendMessageInMessageFieldWithVoiceContent:voiceContent]; } - (IBAction)onListTap:(id)sender { @@ -759,14 +824,11 @@ static UICompositeViewDescription *compositeDescription = nil; } - (IBAction)onMessageChange:(id)sender { - if ([[_messageField text] length] > 0) { - [_sendButton setEnabled:TRUE]; - } else { - [_sendButton setEnabled:FALSE]; - } + [self setSendButtonState]; } - (IBAction)onPictureClick:(id)event { + _preservePendingRecording = true; [_messageField resignFirstResponder]; [ImagePickerView SelectImageFromDevice:self atPosition:_pictureButton inView:self.view withDocumentMenuDelegate:self]; @@ -800,15 +862,15 @@ static UICompositeViewDescription *compositeDescription = nil; #pragma mark ChatRoomDelegate -- (BOOL)startMultiFilesUpload { +- (BOOL)startMultiFilesUploadWithVoiceContent:(LinphoneContent *)voiceContent { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; [fileTransfer setText:[self.messageField text]]; - [fileTransfer uploadFileContent:_fileContext forChatRoom:_chatRoom]; + [fileTransfer uploadFileContent:_fileContext forChatRoom:_chatRoom andVoiceContent:voiceContent]; [_tableController scrollToBottom:true]; return TRUE; } -- (BOOL)startUploadData:(NSData *)data withType:(NSString*)type withName:(NSString *)name andMessage:(NSString *)message { +- (BOOL)startUploadData:(NSData *)data withType:(NSString*)type withName:(NSString *)name andMessage:(NSString *)message voiceContent:(LinphoneContent *)voiceContent { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; if (message) [fileTransfer setText:message]; @@ -818,7 +880,7 @@ static UICompositeViewDescription *compositeDescription = nil; } else if ([type isEqualToString:@"image"]) { key = @"localimage"; } - [fileTransfer uploadData:data forChatRoom:_chatRoom type:type subtype:type name:name key:key]; + [fileTransfer uploadData:data forChatRoom:_chatRoom type:type subtype:type name:name key:key voiceContent:voiceContent]; [_tableController scrollToBottom:true]; return TRUE; } @@ -830,26 +892,26 @@ static UICompositeViewDescription *compositeDescription = nil; return TRUE; } -- (BOOL)resendMultiFiles:(FileContext *)newFileContext message:(NSString *)message { +- (BOOL)resendMultiFiles:(FileContext *)newFileContext message:(NSString *)message voiceContent:(LinphoneContent *)voiceContent { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; if (message) [fileTransfer setText:message]; - [fileTransfer uploadFileContent:newFileContext forChatRoom:_chatRoom]; + [fileTransfer uploadFileContent:newFileContext forChatRoom:_chatRoom andVoiceContent:voiceContent]; [_tableController scrollToBottom:true]; return TRUE; } -- (BOOL)resendFile: (NSData *)data withName:(NSString *)name type:(NSString *)type key:(NSString *)key message:(NSString *)message { +- (BOOL)resendFile: (NSData *)data withName:(NSString *)name type:(NSString *)type key:(NSString *)key message:(NSString *)message voiceContent:(LinphoneContent *)voiceContent{ FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; if (message) [fileTransfer setText:message]; - [fileTransfer uploadData:data forChatRoom:_chatRoom type:type subtype:type name:name key:key]; + [fileTransfer uploadData:data forChatRoom:_chatRoom type:type subtype:type name:name key:key voiceContent:voiceContent]; [_tableController scrollToBottom:true]; return TRUE; } -- (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url { - [self sendMessage:message withExterlBodyUrl:[NSURL URLWithString:url]]; +- (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url voiceContent:(LinphoneContent *)voiceContent { + [self sendMessage:message withExterlBodyUrl:[NSURL URLWithString:url] andVoiceContent:voiceContent]; } #pragma mark ImagePickerDelegate @@ -1077,6 +1139,7 @@ static UICompositeViewDescription *compositeDescription = nil; [_messageView frame].origin.y - tableFrame.origin.y - composeIndicatorCompensation; [_tableController.view setFrame:tableFrame]; + // Scroll to bottom NSInteger lastSection = [_tableController.tableView numberOfSections] - 1; if (lastSection >= 0) { @@ -1102,9 +1165,12 @@ static UICompositeViewDescription *compositeDescription = nil; tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; [_tableController.tableView setFrame:tableViewFrame]; } + if (_showVoiceRecorderView) + _vrView.hidden = true; + [self updateFramesInclRecordingView]; + } completion:^(BOOL finished){ - }]; } @@ -1158,6 +1224,7 @@ static UICompositeViewDescription *compositeDescription = nil; tableFrame.size.height = [_messageView frame].origin.y - tableFrame.origin.y - composeIndicatorCompensation; [_tableController.view setFrame:tableFrame]; + } if ([_fileContext count] > 0){ @@ -1170,6 +1237,7 @@ static UICompositeViewDescription *compositeDescription = nil; CGRect tableViewFrame = [_tableController.tableView frame]; tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; [_tableController.tableView setFrame:tableViewFrame]; + } // Scroll @@ -1183,10 +1251,13 @@ static UICompositeViewDescription *compositeDescription = nil; animated:FALSE]; } } + if (_showVoiceRecorderView) + _vrView.hidden = true; + [self updateFramesInclRecordingView]; + } completion:^(BOOL finished){ - }]; } @@ -1395,7 +1466,7 @@ void on_chat_room_conference_alert(LinphoneChatRoom *cr, const LinphoneEventLog [imgView setUuid:[_fileContext.uuidsArray objectAtIndex:[indexPath item]]]; [imgView setDeleteDelegate:self]; [imgView setFrame:imgFrame]; - [_sendButton setEnabled:TRUE]; + [self setSendButtonState]; return imgView; } @@ -1416,10 +1487,10 @@ void on_chat_room_conference_alert(LinphoneChatRoom *cr, const LinphoneEventLog CGRect tableViewFrame = [_tableController.tableView frame]; tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; [_tableController.tableView setFrame:tableViewFrame]; + [self updateFramesInclRecordingView]; } completion:nil]; - if ([_messageField.text isEqualToString:@""]) - [_sendButton setEnabled:FALSE]; + [self setSendButtonState]; } else { // resizing imagesView CGRect imagesFrame = [_imagesView frame]; @@ -1430,6 +1501,7 @@ void on_chat_room_conference_alert(LinphoneChatRoom *cr, const LinphoneEventLog CGRect tableViewFrame = [_tableController.tableView frame]; tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; [_tableController.tableView setFrame:tableViewFrame]; + [self updateFramesInclRecordingView]; [_imagesCollectionView reloadData]; } } @@ -1592,4 +1664,268 @@ void on_chat_room_conference_alert(LinphoneChatRoom *cr, const LinphoneEventLog } +// Voice redcording + + +- (IBAction)onVrDelete:(id)sender { + [self cancelVoiceRecording]; + [self stopVoiceRecordPlayer]; +} + +- (IBAction)onvrPlayPauseStop:(id)sender { + if (_isVoiceRecording) { + [self stopVoiceRecording]; + } else { + if (_isPlayingVoiceRecording) + [self stopVoiceRecordPlayer]; + else + [self playRecordedMessage]; + } +} + +- (IBAction)onVrStart:(id)sender { + if (_isVoiceRecording) { + [self stopVoiceRecording]; + } else { + [self startVoiceRecording]; + } +} + +-(void) createVoiceRecorder { + LinphoneRecorderParams *p = linphone_core_create_recorder_params(LC); + linphone_recorder_params_set_file_format(p, LinphoneRecorderFileFormatWav); + _voiceRecorder = linphone_core_create_recorder(LC, p); + [CallManager.instance activateAudioSession]; +} + +-(void) cancelVoiceRecording { + _showVoiceRecorderView = false; + _toggleRecord.selected = false; + [self updateFramesInclRecordingView]; + _isPendingVoiceRecord = false; + _isVoiceRecording = false; + if (_voiceRecorder && linphone_recorder_get_state(_voiceRecorder) != LinphoneRecorderClosed) { + linphone_recorder_close(_voiceRecorder); + const char *recordingFile = linphone_recorder_get_file(_voiceRecorder); + if (recordingFile) { + [AppManager removeFileWithFile:[NSString stringWithUTF8String:recordingFile]]; + } + } + [self setSendButtonState]; +} + +-(void) stopVoiceRecording { + if (_voiceRecorder && linphone_recorder_get_state(_voiceRecorder) == LinphoneRecorderRunning) { + LOGI(@"[Chat Message Sending] Pausing / closing voice recorder"); + linphone_recorder_pause(_voiceRecorder); + linphone_recorder_close(_voiceRecorder); + _vrDurationLabel.text = [self formattedDuration:linphone_recorder_get_duration(_voiceRecorder)]; + } + _isVoiceRecording = false; + if ([LinphoneManager.instance lpConfigBoolForKey:@"voice_recording_send_right_away" withDefault:false]) { + [self onSendClick:nil]; + } + [_vrPlayButton setImage:[UIImage imageNamed:@"vr_play"] forState:UIControlStateNormal]; + _toggleRecord.selected = false; + _vrWaveMask.frame = CGRectZero; + [_vrRecordTimer invalidate]; + _isPendingVoiceRecord = linphone_recorder_get_duration(_voiceRecorder) > 0; + [self setSendButtonState]; + +} + +-(void) startVoiceRecording { + + if (!_voiceRecorder) + [self createVoiceRecorder]; + + _toggleRecord.selected = true; + [_vrPlayButton setImage:[UIImage imageNamed:@"vr_stop"] forState:UIControlStateNormal]; + + + _showVoiceRecorderView = true; + [self updateFramesInclRecordingView]; + _isVoiceRecording = true; + _vrWaveMaskPlayer.frame = CGRectZero; + + switch (linphone_recorder_get_state(_voiceRecorder)) { + case LinphoneRecorderClosed: { + NSString *filename = [NSString stringWithFormat:@"%@/voice-recording-%@.wav",[LinphoneManager imagesDirectory], [NSUUID UUID].UUIDString]; + linphone_recorder_open(_voiceRecorder, filename.UTF8String); + linphone_recorder_start(_voiceRecorder); + LOGW(@"[Chat Message Sending] Recorder is closed opening it with %@",filename); + break; + }; + case LinphoneRecorderRunning: { + LOGW(@"[Chat Message Sending] Recorder is already recording"); + break; + } + case LinphoneRecorderPaused: { + LOGW(@"[Chat Message Sending] Recorder isn't closed, resuming recording"); + linphone_recorder_start(_voiceRecorder); + } + } + _vrWaveMask.frame = _vrWave.frame; + _vrDurationLabel.text = [self formattedDuration:linphone_recorder_get_duration(_voiceRecorder)]; + _vrRecordTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector:@selector(voiceRecordTimerUpdate) + userInfo:nil + repeats:YES]; + + +} + +-(void) voiceRecordTimerUpdate { + int recorderDuration = linphone_recorder_get_duration(_voiceRecorder); + if (recorderDuration > [LinphoneManager.instance lpConfigIntForKey:@"voice_recording_max_duration" withDefault:60000]) { + LOGW(@"[Chat Message Sending] Max duration for voice recording exceeded, stopping. (max = %d)",[LinphoneManager.instance lpConfigIntForKey:@"voice_recording_max_duration" withDefault:60000]); + [self stopVoiceRecording]; + } else { + _vrDurationLabel.text = [self formattedDuration:linphone_recorder_get_duration(_voiceRecorder)]; + CGRect r = _vrWaveMask.frame; + r.origin.x += 30; + r.size.width -= 30; + if (r.origin.x > _vrWave.frame.size.width) { + r = _vrWave.frame; + _vrWaveMask.frame = r; + } else { + [UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{ + _vrWaveMask.frame = r; + }completion:^(BOOL finished) {}]; + } + } +} + +// Playback Shared Player (new recording & chat bubble) + +- (void) initSharedPlayer { + LOGI(@"[Voice Message] Creating shared player"); + _sharedVoicePlayer = linphone_core_create_local_player(LC, [CallManager.instance getSpeakerSoundCard].UTF8String, nil, nil); + LinphonePlayerCbs *cbs = linphone_factory_create_player_cbs(linphone_factory_get()); + linphone_player_cbs_set_eof_reached(cbs, on_shared_player_eof_reached); + linphone_player_cbs_set_user_data(cbs, (__bridge void*)self); + linphone_player_add_callbacks(_sharedVoicePlayer, cbs); +} + +-(void) startSharedPlayer:(const char *)path { + LOGI(@"[Voice Message] Starting shared player path = %s",path); + if (linphone_player_get_user_data(_sharedVoicePlayer)) { + LOGI(@"[Voice Message] a play was requested (%s), but there is already one going (%s)",path,(const char *)linphone_player_get_user_data(_sharedVoicePlayer) ); + NSDictionary* userInfo = @{@"path": [NSString stringWithUTF8String:linphone_player_get_user_data(_sharedVoicePlayer)]}; + [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneVoiceMessagePlayerLostFocus object:nil userInfo:userInfo]; + } + [CallManager.instance changeRouteToSpeaker]; + linphone_player_set_user_data(_sharedVoicePlayer, (void *)path); + linphone_player_open(_sharedVoicePlayer, path); + linphone_player_start(_sharedVoicePlayer); +} + +-(void) stopSharedPlayer { + LOGI(@"[Voice Message] Stopping shared player path = %s",linphone_player_get_user_data(_sharedVoicePlayer) ? (const char *)linphone_player_get_user_data(_sharedVoicePlayer) : "nil"); + linphone_player_pause(_sharedVoicePlayer); + linphone_player_seek(_sharedVoicePlayer,0); + linphone_player_close(_sharedVoicePlayer); + linphone_player_set_user_data(_sharedVoicePlayer, nil); +} + +-(BOOL) sharedPlayedIsPlaying:(const char *)path { + return path && linphone_player_get_user_data(_sharedVoicePlayer) && !strcmp(path,linphone_player_get_user_data(_sharedVoicePlayer)); +} + +void on_shared_player_eof_reached(LinphonePlayer *p) { + LOGI(@"[Voice Message] End of file reached for player"); + const char * currentPlayedFile = (const char *) linphone_player_get_user_data(p); + if (currentPlayedFile) { + NSDictionary* userInfo = @{@"path": [NSString stringWithUTF8String:currentPlayedFile]}; + [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneVoiceMessagePlayerEOF object:nil userInfo:userInfo]; + } + +// ChatConversationView *view = (__bridge ChatConversationView *)linphone_player_cbs_get_user_data(linphone_player_get_current_callbacks(p)); +// [view stopVoiceRecordPlayer]; +} + +// Playback of new recordings + +-(void) playRecordedMessage { + [_vrPlayButton setImage:[UIImage imageNamed:@"vr_stop"] forState:UIControlStateNormal]; + _vrDurationLabel.text = [self formattedDuration:linphone_player_get_duration(_sharedVoicePlayer)]; + _vrWaveMask.frame = CGRectZero; + CGRect r = CGRectZero; + r.size.height = _vrInnerView.frame.size.height; + _vrWaveMaskPlayer.frame = r; + _vrPlayerTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector:@selector(voicePlayTimerUpdate) + userInfo:nil + repeats:YES]; + [self startSharedPlayer:linphone_recorder_get_file(_voiceRecorder)]; + [self animPlayerOnce]; + _isPlayingVoiceRecording = true; +} + +-(void) voicePlayTimerUpdate { + _vrDurationLabel.text = [self formattedDuration:linphone_player_get_duration(_sharedVoicePlayer)]; + [self animPlayerOnce]; +} + +-(void) animPlayerOnce { + CGRect r = _vrWaveMaskPlayer.frame; + r.size.width += _vrInnerView.frame.size.width / ((linphone_player_get_duration(_sharedVoicePlayer) / 1000)+1) ; + if (r.size.width > _vrInnerView.frame.size.width) { + r.size.width = _vrInnerView.frame.size.width; + } + [UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{ + _vrWaveMaskPlayer.frame = r; + }completion:^(BOOL finished) {}]; +} + +-(void) endVoicePlayingIfDoingSO:(NSNotification *)notif { + if (_isPlayingVoiceRecording) + [self stopVoiceRecordPlayer]; +} + +-(void) stopVoiceRecordPlayer { + [self stopSharedPlayer]; + [_vrPlayButton setImage:[UIImage imageNamed:@"vr_play"] forState:UIControlStateNormal]; + _isPlayingVoiceRecording = false; + [_vrPlayerTimer invalidate]; + _vrWaveMaskPlayer.frame = CGRectZero; +} + +-(NSString *)formattedDuration:(long)valueMs { + return [NSString stringWithFormat:@"%02ld:%02ld", valueMs/ 60000, (valueMs % 60000) / 1000 ]; +} + +-(void) updateFramesInclRecordingView { // place below the messages table. + BOOL showHide = _showVoiceRecorderView != !_vrView.hidden; + if (showHide) + _vrView.hidden = !_showVoiceRecorderView; + + CGRect vrFrame = _vrView.frame; + CGRect tableFrame = _tableController.tableView.frame; + if (showHide) { + tableFrame.size.height = _showVoiceRecorderView ? tableFrame.size.height - vrFrame.size.height : tableFrame.size.height + vrFrame.size.height; + _tableController.tableView.frame = tableFrame; + [_tableController.tableView reloadData]; + } + vrFrame.origin.y = tableFrame.origin.y+tableFrame.size.height; + _vrView.frame = vrFrame; +} + +-(void) stopAllPlays { + if (linphone_player_get_user_data(_sharedVoicePlayer)) { + NSDictionary* userInfo = @{@"path": [NSString stringWithUTF8String:linphone_player_get_user_data(_sharedVoicePlayer)]}; + [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneVoiceMessagePlayerLostFocus object:nil userInfo:userInfo]; + } +} + +// send button state + +-(void) setSendButtonState { + _sendButton.enabled = !_isVoiceRecording && ((_isPendingVoiceRecord && linphone_recorder_get_duration(_voiceRecorder) > 0) || [[_messageField text] length] > 0 || _fileContext.count > 0); +} + + + @end diff --git a/Classes/FirstLoginView.m b/Classes/FirstLoginView.m index f00b1182d..a0d10d0a6 100644 --- a/Classes/FirstLoginView.m +++ b/Classes/FirstLoginView.m @@ -200,7 +200,7 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)onLoginClick:(id)sender { if (!linphone_core_is_network_reachable(LC)) { - [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView] animated:YES completion:nil]; + [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView:@"configure an account"] animated:YES completion:nil]; return; } diff --git a/Classes/LinphoneManager.h b/Classes/LinphoneManager.h index f168ada5a..1caf894f7 100644 --- a/Classes/LinphoneManager.h +++ b/Classes/LinphoneManager.h @@ -60,6 +60,8 @@ extern NSString *const kLinphoneFileTransferRecvUpdate; extern NSString *const kLinphoneQRCodeFound; extern NSString *const kLinphoneChatCreateViewChange; extern NSString *const kLinphoneEphemeralMessageDeletedInRoom; +extern NSString *const kLinphoneVoiceMessagePlayerEOF; +extern NSString *const kLinphoneVoiceMessagePlayerLostFocus; extern NSString *const kLinphoneMsgNotificationAppGroupId; diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index 64bb1b296..47b288df4 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -75,6 +75,8 @@ NSString *const kLinphoneFileTransferRecvUpdate = @"LinphoneFileTransferRecvUpda NSString *const kLinphoneQRCodeFound = @"LinphoneQRCodeFound"; NSString *const kLinphoneChatCreateViewChange = @"LinphoneChatCreateViewChange"; NSString *const kLinphoneEphemeralMessageDeletedInRoom = @"LinphoneEphemeralMessageDeletedInRoom"; +NSString *const kLinphoneVoiceMessagePlayerEOF = @"LinphoneVoiceMessagePlayerEOF"; +NSString *const kLinphoneVoiceMessagePlayerLostFocus = @"LinphoneVoiceMessagePlayerLostFocus"; NSString *const kLinphoneMsgNotificationAppGroupId = @"group.org.linphone.phone.msgNotification"; @@ -1784,7 +1786,7 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { - (void)call:(const LinphoneAddress *)iaddr { // First verify that network is available, abort otherwise. if (!linphone_core_is_network_reachable(theLinphoneCore)) { - [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView] animated:YES completion:nil]; + [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView:@"place a call"] animated:YES completion:nil]; return; } diff --git a/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib b/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib index d320d6580..b987f08b4 100644 --- a/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib +++ b/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib @@ -33,11 +33,16 @@ + + + + + - + @@ -57,11 +62,11 @@ - + - + @@ -158,28 +163,60 @@ - + + + + + + + + + + + + + + + + + + + + @@ -217,5 +254,7 @@ + + diff --git a/Classes/LinphoneUI/UIChatBubblePhotoCell.h b/Classes/LinphoneUI/UIChatBubblePhotoCell.h index 43b6b6eed..c1530c413 100644 --- a/Classes/LinphoneUI/UIChatBubblePhotoCell.h +++ b/Classes/LinphoneUI/UIChatBubblePhotoCell.h @@ -44,6 +44,16 @@ @property (strong, nonatomic) IBOutlet UILongPressGestureRecognizer *plusLongGestureRecognizer; @property(strong, nonatomic) NSMutableArray *contentViews; +// Video recordings +@property (weak, nonatomic) IBOutlet UIView *vrView; +@property (weak, nonatomic) IBOutlet UIButton *vrPlayPause; +@property (weak, nonatomic) IBOutlet UILabel *vrTimerLabel; +@property (weak, nonatomic) IBOutlet UIImageView *vrWave; +@property (weak, nonatomic) IBOutlet UIView *vrWaveMaskPlayback; +@property NSTimer *vrPlayerTimer; +@property NSString *voiceRecordingFile; + + - (void)setEvent:(LinphoneEventLog *)event; - (void)setChatMessage:(LinphoneChatMessage *)message; diff --git a/Classes/LinphoneUI/UIChatBubblePhotoCell.m b/Classes/LinphoneUI/UIChatBubblePhotoCell.m index d96ae3cb2..4e6766faa 100644 --- a/Classes/LinphoneUI/UIChatBubblePhotoCell.m +++ b/Classes/LinphoneUI/UIChatBubblePhotoCell.m @@ -26,6 +26,11 @@ #import #import +#define voicePlayer VIEW(ChatConversationView).sharedVoicePlayer +#define chatView VIEW(ChatConversationView) + + + @implementation UIChatBubblePhotoCell { FileTransferDelegate *_ftd; CGSize imageSize, bubbleSize, videoDefaultSize; @@ -54,6 +59,8 @@ assetIsLoaded = FALSE; self.contentView.userInteractionEnabled = NO; _contentViews = [[NSMutableArray alloc] init]; + self.vrWaveMaskPlayback.layer.cornerRadius = 10.0f; + self.vrWaveMaskPlayback.layer.masksToBounds = YES; } return self; } @@ -151,32 +158,55 @@ }); } + - (void)update { if (self.message == nil) { LOGW(@"Cannot update message room cell: NULL message"); return; } [super update]; - + + NSMutableDictionary *encrptedFilePaths = NULL; + if ([VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId]) { + encrptedFilePaths = [LinphoneManager getMessageAppDataForKey:@"encryptedfiles" inMessage:self.message]; + if (!encrptedFilePaths) { + encrptedFilePaths = [NSMutableDictionary dictionary]; + } + } + + _voiceRecordingFile = nil; + LinphoneContent *voiceContent = [UIChatBubbleTextCell voiceContent:self.message]; + if (voiceContent) { + _voiceRecordingFile = [NSString stringWithUTF8String:[VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId] ? linphone_content_get_plain_file_path(voiceContent) : linphone_content_get_file_path(voiceContent)]; + if ([VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId]) + [encrptedFilePaths setValue:_voiceRecordingFile forKey:[NSString stringWithUTF8String:linphone_content_get_name(voiceContent)]]; + [self setVoiceMessageDuration]; + _vrWaveMaskPlayback.frame = CGRectZero; + _vrWaveMaskPlayback.backgroundColor = linphone_chat_message_is_outgoing(self.message) ? UIColor.orangeColor : UIColor.grayColor; + } + const bctbx_list_t *contents = linphone_chat_message_get_contents(self.message); + + size_t contentCount = bctbx_list_size(contents); + if (voiceContent) + contentCount--; BOOL multiParts = ((linphone_chat_message_get_text_content(self.message) != NULL) ? bctbx_list_size(contents) > 2 : bctbx_list_size(contents) > 1); + if (voiceContent && !multiParts) { + _cancelButton.hidden = _fileTransferProgress.hidden = _downloadButton.hidden = _playButton.hidden = _fileName.hidden = _fileView.hidden = _fileButton.hidden = YES; + return; + } + if (multiParts) { if (!assetIsLoaded) { - NSMutableDictionary *encrptedFilePaths = NULL; - if ([VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId]) { - encrptedFilePaths = [LinphoneManager getMessageAppDataForKey:@"encryptedfiles" inMessage:self.message]; - if (!encrptedFilePaths) { - encrptedFilePaths = [NSMutableDictionary dictionary]; - } - } - _imageGestureRecognizer.enabled = NO; _cancelButton.hidden = _fileTransferProgress.hidden = _downloadButton.hidden = _playButton.hidden = _fileName.hidden = _fileView.hidden = _fileButton.hidden = YES; - const bctbx_list_t *it = contents; int i; for (it = contents, i=0; it != NULL; it=bctbx_list_next(it)){ LinphoneContent *content = (LinphoneContent *)it->data; + if (linphone_content_is_voice_recording(content)) { // Handled elsewhere + continue; + } if (linphone_content_is_file_transfer(content) || linphone_content_is_file(content)){ UIChatContentView *contentView = [[UIChatContentView alloc] initWithFrame: CGRectMake(0,0,0,0)]; if([VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId] && (linphone_chat_message_is_outgoing(self.message) || linphone_content_is_file(content))) { @@ -214,8 +244,6 @@ return; } - - const char *url = linphone_chat_message_get_external_body_url(self.message); BOOL is_external = (url && (strstr(url, "http") == url)) || linphone_chat_message_get_file_transfer_information(self.message); @@ -713,16 +741,95 @@ textFrame.origin = CGPointMake(textFrame.origin.x, self.finalAssetView.frame.origin.y + self.finalAssetView.frame.size.height); else // When image hasn't be download - textFrame.origin = CGPointMake(textFrame.origin.x, _imageSubView.frame.size.height + _imageSubView.frame.origin.y - 10); + textFrame.origin = CGPointMake(textFrame.origin.x, _voiceRecordingFile ? _fileView.frame.origin.y : _imageSubView.frame.size.height + _imageSubView.frame.origin.y - 10); if (!utf8Text) { textFrame.size.height = 0; } else { textFrame.size.height = bubbleFrame.size.height - 90;//textFrame.origin.x; } + + if (_voiceRecordingFile) { + CGRect vrFrame = _vrView.frame; + vrFrame.origin.y = _contentViews.count == 0 && !utf8Text ? _fileView.frame.origin.y : textFrame.origin.y; + _vrView.frame = vrFrame; + textFrame.origin.y += VOICE_RECORDING_PLAYER_HEIGHT; + _vrView.hidden = NO; + } else { + _vrView.hidden = YES; + } self.messageText.frame = textFrame; } +// Voice messages + +static AVAudioPlayer* utilityPlayer; + +-(void) setVoiceMessageDuration { + NSError *error = nil; + AVAudioPlayer* utilityPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL URLWithString:_voiceRecordingFile] error:&error]; // Workaround as opening multiple linphone_players at the same time can cause crash (here for example layout refreshed whilst a voice memo is playing + _vrTimerLabel.text = [self formattedDuration:utilityPlayer.duration]; + utilityPlayer = nil; +} + +-(void) voicePlayTimerUpdate { + CGRect r = _vrWaveMaskPlayback.frame; + r.size.width += _vrView.frame.size.width / ((linphone_player_get_duration(voicePlayer) / 500)) ; + if (r.size.width > _vrView.frame.size.width) { + r.size.width = _vrView.frame.size.width; + } + [UIView animateWithDuration:0.5 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{ + _vrWaveMaskPlayback.frame = r; + }completion:^(BOOL finished) {}]; +} + + +-(void) stopPlayer { + [NSNotificationCenter.defaultCenter removeObserver:self]; + [chatView stopSharedPlayer]; + [_vrPlayPause setImage:[UIImage imageNamed:@"vr_play"] forState:UIControlStateNormal]; + [_vrPlayerTimer invalidate]; + _vrWaveMaskPlayback.frame = CGRectZero; +} + +-(NSString *)formattedDuration:(long)valueMs { + return [NSString stringWithFormat:@"%02ld:%02ld", valueMs/ 60, (valueMs % 60) ]; +} + +-(void) startPlayer { + [chatView startSharedPlayer:_voiceRecordingFile.UTF8String]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stopPlayer) + name:kLinphoneVoiceMessagePlayerLostFocus + object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stopPlayer) + name:kLinphoneVoiceMessagePlayerEOF + object:nil]; + + [_vrPlayPause setImage:[UIImage imageNamed:@"vr_stop"] forState:UIControlStateNormal]; + CGRect r = CGRectZero; + r.size.height = _vrView.frame.size.height - 14; + r.origin.y = 7; + _vrWaveMaskPlayback.frame = r; + _vrPlayerTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 + target:self + selector:@selector(voicePlayTimerUpdate) + userInfo:nil + repeats:YES]; + [self voicePlayTimerUpdate]; + +} + +- (IBAction)onVRPlayPauseClick:(id)sender { + if ([chatView sharedPlayedIsPlaying:_voiceRecordingFile.UTF8String]) + [self stopPlayer]; + else { + [self startPlayer]; + } +} + @end diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.h b/Classes/LinphoneUI/UIChatBubbleTextCell.h index 6778a8143..062164d5f 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.h +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.h @@ -26,6 +26,9 @@ #define CELL_IMAGE_X_MARGIN 100 #define IMAGE_DEFAULT_WIDTH 120 #define IMAGE_DEFAULT_MARGIN 5 +#define VOICE_RECORDING_PLAYER_HEIGHT 60 +#define VOICE_RECORDING_PLAYER_WIDTH 300 + @interface UIChatBubbleTextCell : UITableViewCell @@ -72,5 +75,6 @@ + (NSString *)TextMessageForChat:(LinphoneChatMessage *)message; + (CGSize)computeBoundingBox:(NSString *)text size:(CGSize)size font:(UIFont *)font; + (NSString *)ContactDateForChat:(LinphoneChatMessage *)message; ++(LinphoneContent *) voiceContent:(LinphoneChatMessage *)message; @end diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.m b/Classes/LinphoneUI/UIChatBubbleTextCell.m index 808cb20b1..fc3371774 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.m +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m @@ -306,6 +306,12 @@ } - (void)onResend { + + if (!linphone_core_is_network_reachable(LC)) { + [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView:@"send a message"] animated:YES completion:nil]; + //return; + } + if (_message == nil || !linphone_chat_message_is_outgoing(_message)) return; @@ -314,7 +320,13 @@ return; const bctbx_list_t *contents = linphone_chat_message_get_contents(_message); - BOOL multiParts = ((linphone_chat_message_get_text_content(self.message) != NULL) ? bctbx_list_size(contents) > 2 : bctbx_list_size(contents) > 1); + LinphoneContent *voiceContent = [UIChatBubbleTextCell voiceContent:_message]; + size_t contentCount = bctbx_list_size(contents); + if (voiceContent) + contentCount--; + + BOOL multiParts = ((linphone_chat_message_get_text_content(_message) != NULL) ? contentCount > 2 : contentCount > 1); + if (multiParts) { FileContext *newfileContext = [[FileContext alloc] init]; [newfileContext clear]; @@ -323,6 +335,9 @@ const bctbx_list_t *it; for (it = contents, i=0; it != NULL; it=bctbx_list_next(it)){ LinphoneContent *content = (LinphoneContent *)it->data; + if (linphone_content_is_voice_recording(content)) { + continue; + } if (linphone_content_is_file_transfer(content) || linphone_content_is_file(content)){ NSString *name = [NSString stringWithUTF8String:linphone_content_get_name(content)]; NSString *filePath = [encrptedFilePaths valueForKey:name]; @@ -335,11 +350,11 @@ [self onDelete]; dispatch_async(dispatch_get_main_queue(), ^ { const char *text = linphone_chat_message_get_text_content(_message); - [_chatRoomDelegate resendMultiFiles:newfileContext message: text? [NSString stringWithUTF8String:text]: NULL]; + [_chatRoomDelegate resendMultiFiles:newfileContext message: text? [NSString stringWithUTF8String:text]: NULL voiceContent:voiceContent]; }); return; } - if (linphone_chat_message_get_file_transfer_information(_message) != NULL) { + if (!voiceContent && contentCount == 1 && linphone_chat_message_get_file_transfer_information(_message) != NULL) { NSString *localImage = [LinphoneManager getMessageAppDataForKey:@"localimage" inMessage:_message]; NSString *localVideo = [LinphoneManager getMessageAppDataForKey:@"localvideo" inMessage:_message]; NSString *localFile = [LinphoneManager getMessageAppDataForKey:@"localfile" inMessage:_message]; @@ -354,11 +369,11 @@ const char *text = linphone_chat_message_get_text_content(_message); NSString *str = text ? [NSString stringWithUTF8String:text] : NULL; if (localImage) { - [_chatRoomDelegate resendFile: (data?:[ChatConversationView getFileData:localImage]) withName:localImage type:@"image" key:@"localimage" message:str]; + [_chatRoomDelegate resendFile: (data?:[ChatConversationView getFileData:localImage]) withName:localImage type:@"image" key:@"localimage" message:str voiceContent:voiceContent]; } else if (localVideo) { - [_chatRoomDelegate resendFile:(data?:[ChatConversationView getFileData:localVideo]) withName:localVideo type:@"video" key:@"localvideo" message:str]; + [_chatRoomDelegate resendFile:(data?:[ChatConversationView getFileData:localVideo]) withName:localVideo type:@"video" key:@"localvideo" message:str voiceContent:voiceContent]; } else { - [_chatRoomDelegate resendFile:(data?:[ChatConversationView getFileData:localFile]) withName:localFile type:@"image" key:@"localfile" message:str]; + [_chatRoomDelegate resendFile:(data?:[ChatConversationView getFileData:localFile]) withName:localFile type:@"image" key:@"localfile" message:str voiceContent:voiceContent]; } }); } else { @@ -366,7 +381,10 @@ double delayInSeconds = 0.4; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { - [_chatRoomDelegate resendChat:self.textMessage withExternalUrl:nil]; + NSString *text = self.textMessage; + if (voiceContent && [text isEqualToString:@"🗻"]) + text = nil; + [_chatRoomDelegate resendChat:text withExternalUrl:nil voiceContent:voiceContent]; }); } } @@ -457,6 +475,20 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; return image; } ++(LinphoneContent *) voiceContent:(LinphoneChatMessage *)message { + for (const bctbx_list_t *it = linphone_chat_message_get_contents(message); it != NULL; it=bctbx_list_next(it)){ + LinphoneContent *content = (LinphoneContent *)it->data; + if (linphone_content_is_voice_recording(content)) + return content; + } + return nil; +} + + ++(CGSize) addVoicePlayerToSize:(CGSize)size withMargins:(BOOL)margins { + return CGSizeMake(MAX(size.width,VOICE_RECORDING_PLAYER_WIDTH + (margins ? CELL_MESSAGE_X_MARGIN: 0)), size.height + VOICE_RECORDING_PLAYER_HEIGHT+(margins ? CELL_MESSAGE_Y_MARGIN: 0)); + +} + (CGSize)ViewHeightForMessageText:(LinphoneChatMessage *)chat withWidth:(int)width textForImdn:(NSString *)imdnText { NSString *messageText = [UIChatBubbleTextCell TextMessageForChat:chat]; @@ -484,14 +516,51 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; CGFloat imagesh=0; CGFloat max_imagesw=0; CGFloat max_imagesh=0; + LinphoneContent *voiceContent = [self voiceContent:chat]; const bctbx_list_t *contents = linphone_chat_message_get_contents(chat); - BOOL multiParts = ((linphone_chat_message_get_text_content(chat) != NULL) ? bctbx_list_size(contents) > 2 : bctbx_list_size(contents) > 1); + size_t contentCount = bctbx_list_size(contents); + if (voiceContent) + contentCount--; + + BOOL multiParts = ((linphone_chat_message_get_text_content(chat) != NULL) ? contentCount > 2 : contentCount > 1); + + if (voiceContent && contentCount == 0) { + size = CGSizeMake(VOICE_RECORDING_PLAYER_WIDTH, VOICE_RECORDING_PLAYER_HEIGHT); + CGSize textSize = CGSizeMake(0, 0); + if (![messageText isEqualToString:@"🗻"]) { + textSize = [self computeBoundingBox:messageText + size:CGSizeMake(max_imagesw , CGFLOAT_MAX) + font:messageFont]; + } + + // add size for message text + size.height += textSize.height; + size.width = MAX(textSize.width, size.width); + size.width = MAX(size.width + CELL_MESSAGE_X_MARGIN, CELL_MIN_WIDTH); + size.height = MAX(size.height + CELL_MESSAGE_Y_MARGIN, CELL_MIN_HEIGHT) ; + return size; + } + if (multiParts) { const bctbx_list_t *it = contents; NSMutableDictionary *encrptedFilePaths = [LinphoneManager getMessageAppDataForKey:@"encryptedfiles" inMessage:chat]; for (it = contents; it != NULL; it=bctbx_list_next(it)){ LinphoneContent *content = (LinphoneContent *)it->data; + if (linphone_content_is_voice_recording(content)) { + CGSize sSize = CGSizeMake(VOICE_RECORDING_PLAYER_WIDTH, VOICE_RECORDING_PLAYER_HEIGHT); + imagesw += sSize.width; + if (imagesw > width) { + imagesw = sSize.width; + max_imagesw = MAX(max_imagesw, imagesw); + max_imagesh += imagesh; + imagesh = sSize.height; + } else { + max_imagesw = MAX(max_imagesw, imagesw); + imagesh = MAX(imagesh, sSize.height); + } + continue; + } UIImage *image; if(!linphone_chat_message_is_outgoing(chat) && linphone_content_is_file_transfer(content)) { // not yet downloaded @@ -538,14 +607,15 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; size.height = MAX(size.height + CELL_MESSAGE_Y_MARGIN, CELL_MIN_HEIGHT) ; return size; } + - - LinphoneContent *fileContent = linphone_chat_message_get_file_transfer_information(chat); + LinphoneContent *fileContent = linphone_chat_message_get_utf8_text(chat) ? nil : linphone_chat_message_get_file_transfer_information(chat); if (url == nil && fileContent == NULL) { size = [self computeBoundingBox:messageText size:CGSizeMake(width - CELL_MESSAGE_X_MARGIN - 4, CGFLOAT_MAX) font:messageFont]; } else { + NSString *localImage = [LinphoneManager getMessageAppDataForKey:@"localimage" inMessage:chat]; NSString *localFile = [LinphoneManager getMessageAppDataForKey:@"localfile" inMessage:chat]; NSString *localVideo = [LinphoneManager getMessageAppDataForKey:@"localvideo" inMessage:chat]; @@ -583,27 +653,43 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; image = [[UIImage alloc] initWithData:data]; } } else { - return [self ViewHeightForFile:width]; + CGSize fileSize = [self ViewHeightForFile:width]; + if (voiceContent) { + fileSize = [self addVoicePlayerToSize:fileSize withMargins:true]; + } + return fileSize; } originalImageSize = image.size; } else { if (!localImage && !localVideo) { //We are loading the image - return CGSizeMake(CELL_MIN_WIDTH + CELL_MESSAGE_X_MARGIN, CELL_MIN_HEIGHT + CELL_MESSAGE_Y_MARGIN + textSize.height + 20); + CGSize baseSize = CGSizeMake(CELL_MIN_WIDTH + CELL_MESSAGE_X_MARGIN, CELL_MIN_HEIGHT + CELL_MESSAGE_Y_MARGIN + textSize.height + 20); + if (voiceContent) { + baseSize = [self addVoicePlayerToSize:baseSize withMargins:true]; + } + return baseSize; } if (localImage && [[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSData* data = [NSData dataWithContentsOfFile:filePath]; UIImage *image = [[UIImage alloc] initWithData:data]; if (!image) { - return [self ViewHeightForFile:width]; + CGSize fileSize = [self ViewHeightForFile:width]; + if (voiceContent) { + fileSize = [self addVoicePlayerToSize:fileSize withMargins:true]; + } + return fileSize; } originalImageSize = image.size; } else if (localVideo && [[NSFileManager defaultManager] fileExistsAtPath:filePath]) { UIImage *image = [UIChatBubbleTextCell getImageFromVideoUrl:[NSURL fileURLWithPath:filePath]]; if (!image) { - return [self ViewHeightForFile:width]; + CGSize fileSize = [self ViewHeightForFile:width]; + if (voiceContent) { + fileSize = [self addVoicePlayerToSize:fileSize withMargins:true]; + } + return fileSize; } originalImageSize = image.size; } else { @@ -615,7 +701,11 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:localVideo] options:nil]; if (![assets firstObject]) { - return CGSizeMake(CELL_MIN_WIDTH, CELL_MIN_WIDTH + CELL_MESSAGE_Y_MARGIN + textSize.height); + CGSize baseSize = CGSizeMake(CELL_MIN_WIDTH, CELL_MIN_WIDTH + CELL_MESSAGE_Y_MARGIN + textSize.height); + if (voiceContent) { + baseSize = [self addVoicePlayerToSize:baseSize withMargins:true]; + } + return baseSize; } else { PHAsset *asset = [assets firstObject]; originalImageSize = CGSizeMake([asset pixelWidth], [asset pixelHeight]); @@ -627,6 +717,11 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 44; size.height += textSize.height; size.width = MAX(textSize.width, size.width); } + + if (voiceContent) { + size.width = MAX(size.width,VOICE_RECORDING_PLAYER_WIDTH); + size.height += VOICE_RECORDING_PLAYER_HEIGHT; + } size.width = MAX(size.width + CELL_MESSAGE_X_MARGIN, CELL_MIN_WIDTH); size.height = MAX(size.height + CELL_MESSAGE_Y_MARGIN, CELL_MIN_HEIGHT); diff --git a/Classes/PhoneMainView.m b/Classes/PhoneMainView.m index c8d78cd4f..680712541 100644 --- a/Classes/PhoneMainView.m +++ b/Classes/PhoneMainView.m @@ -830,7 +830,7 @@ static RootViewManager *rootViewManagerInstance = nil; } if (!linphone_core_is_network_reachable(LC)) { - [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView] animated:YES completion:nil]; + [PhoneMainView.instance presentViewController:[LinphoneUtils networkErrorView:@"send a message"] animated:YES completion:nil]; return; } diff --git a/Classes/Utils/FileTransferDelegate.h b/Classes/Utils/FileTransferDelegate.h index 3aa3d22db..21ff1d605 100644 --- a/Classes/Utils/FileTransferDelegate.h +++ b/Classes/Utils/FileTransferDelegate.h @@ -24,8 +24,8 @@ @interface FileTransferDelegate : NSObject -- (void)uploadFileContent: (FileContext *)context forChatRoom:(LinphoneChatRoom *)chatRoom; -- (void)uploadData:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom type:(NSString *)type subtype:(NSString *)subtype name:(NSString *)name key:(NSString *)key; +- (void)uploadFileContent: (FileContext *)context forChatRoom:(LinphoneChatRoom *)chatRoom andVoiceContent:(LinphoneContent *)voiceContent; +- (void)uploadData:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom type:(NSString *)type subtype:(NSString *)subtype name:(NSString *)name key:(NSString *)key voiceContent:(LinphoneContent *)voiceContent; - (void)uploadImage:(UIImage *)image forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality; - (void)uploadFile:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom withName:(NSString *)name; - (void)uploadVideo:(NSData *)data withassetId:(NSString *)phAssetId forChatRoom:(LinphoneChatRoom *)chatRoom; diff --git a/Classes/Utils/FileTransferDelegate.m b/Classes/Utils/FileTransferDelegate.m index d8d20670b..9ad65457e 100644 --- a/Classes/Utils/FileTransferDelegate.m +++ b/Classes/Utils/FileTransferDelegate.m @@ -102,7 +102,7 @@ static void file_transfer_progress_indication_send(LinphoneChatMessage *message, } } -- (void)uploadData:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom type:(NSString *)type subtype:(NSString *)subtype name:(NSString *)name key:(NSString *)key{ +- (void)uploadData:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom type:(NSString *)type subtype:(NSString *)subtype name:(NSString *)name key:(NSString *)key voiceContent:(LinphoneContent *)voiceContent{ if ([[LinphoneManager.instance fileTransferDelegates] containsObject:self]) { LOGW(@"fileTransferDelegates has already added %p", self); return; @@ -124,12 +124,15 @@ static void file_transfer_progress_indication_send(LinphoneChatMessage *message, linphone_chat_message_cbs_set_file_transfer_progress_indication(linphone_chat_message_get_callbacks(_message), file_transfer_progress_indication_send); [LinphoneManager setValueInMessageAppData:name forKey:key inMessage:_message]; + + if (voiceContent) + linphone_chat_message_add_content(_message, voiceContent); LOGI(@"%p Uploading content from message %p", self, _message); linphone_chat_message_send(_message); } -- (void)uploadFileContent: (FileContext *)context forChatRoom:(LinphoneChatRoom *)chatRoom { +- (void)uploadFileContent: (FileContext *)context forChatRoom:(LinphoneChatRoom *)chatRoom andVoiceContent:(LinphoneContent *)voiceContent{ [LinphoneManager.instance.fileTransferDelegates addObject:self]; _message = linphone_chat_room_create_empty_message(chatRoom); @@ -162,6 +165,8 @@ static void file_transfer_progress_indication_send(LinphoneChatMessage *message, // todo indication progress [LinphoneManager setValueInMessageAppData:names forKey:@"multiparts" inMessage:_message]; [LinphoneManager setValueInMessageAppData:types forKey:@"multipartstypes" inMessage:_message]; + if (voiceContent) + linphone_chat_message_add_content(_message, voiceContent); LOGI(@"%p Uploading content from message %p", self, _message); linphone_chat_message_send(_message); } @@ -170,12 +175,12 @@ static void file_transfer_progress_indication_send(LinphoneChatMessage *message, - (void)uploadImage:(UIImage *)image forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality { NSString *name = [NSString stringWithFormat:@"%li-%f.jpg", (long)image.hash, [NSDate timeIntervalSinceReferenceDate]]; NSData *data = UIImageJPEGRepresentation(image, quality); - [self uploadData:data forChatRoom:chatRoom type:@"image" subtype:@"jpg" name:name key:@"localimage"]; + [self uploadData:data forChatRoom:chatRoom type:@"image" subtype:@"jpg" name:name key:@"localimage" voiceContent:nil]; } - (void)uploadVideo:(NSData *)data withassetId:(NSString *)phAssetId forChatRoom:(LinphoneChatRoom *)chatRoom { NSString *name = [NSString stringWithFormat:@"IMG-%f.MOV", [NSDate timeIntervalSinceReferenceDate]]; - [self uploadData:data forChatRoom:chatRoom type:@"video" subtype:@"mov" name:name key:@"localvideo"]; + [self uploadData:data forChatRoom:chatRoom type:@"video" subtype:@"mov" name:name key:@"localvideo" voiceContent:nil]; } - (void)uploadFile:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom withName:(NSString *)name { @@ -184,7 +189,7 @@ static void file_transfer_progress_indication_send(LinphoneChatMessage *message, NSString *fileType = [[asset tracksWithMediaType:AVMediaTypeVideo] count] > 0 ? @"video" : @"file"; NSString *key = [ChatConversationView getKeyFromFileType:fileType fileName:name]; - [self uploadData:data forChatRoom:chatRoom type:fileType subtype:name.lastPathComponent name:name key:key]; + [self uploadData:data forChatRoom:chatRoom type:fileType subtype:name.lastPathComponent name:name key:key voiceContent:nil]; } - (BOOL)download:(LinphoneChatMessage *)message { diff --git a/Classes/Utils/Utils.h b/Classes/Utils/Utils.h index 7289bbe83..d515926af 100644 --- a/Classes/Utils/Utils.h +++ b/Classes/Utils/Utils.h @@ -39,7 +39,7 @@ + (UIImage *)resizeImage:(UIImage *)imageToResize newSize:(CGSize)newSize; + (LinphoneAddress *)normalizeSipOrPhoneAddress:(NSString *)addr; -+ (UIAlertController *)networkErrorView; ++ (UIAlertController *)networkErrorView:(NSString *)action; typedef enum { LinphoneDateHistoryList, diff --git a/Classes/Utils/Utils.m b/Classes/Utils/Utils.m index 4f6d1249d..7ae1fedca 100644 --- a/Classes/Utils/Utils.m +++ b/Classes/Utils/Utils.m @@ -516,12 +516,10 @@ return res; } -+ (UIAlertController *)networkErrorView { ++ (UIAlertController *)networkErrorView:(NSString *)action { UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Network Error", nil) - message:NSLocalizedString(@"There is no network connection available, " - @"enable WIFI or WWAN prior to place a call", - nil) + message:NSLocalizedString([@"There is no network connection available, enable WIFI or WWAN prior to " stringByAppendingString:action],nil) preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index a6921777283f02b6a3459d925a1e1ca1157e6be4..76f33d13e5ef9b0b98da692eded9ac33c05d1570 100644 GIT binary patch delta 68 zcmZ29i@9qq^9Hrn$tN1RCZE~PHTjLH5W5nC0)s7s!sLSu6JcVkN(|NvT$2;qk@YWry3e} delta 34 ocmeC0%e-h7^9Hrn$vK@IlN~zvCO>Oam^`mj1jOF_tZj)q0024<9smFU diff --git a/Resources/fr.lproj/Localizable.strings b/Resources/fr.lproj/Localizable.strings index b119be4b7285149ae778d8e5445b958e36e3a61d..318063fa67dcd23b218657727217bd4acbf5c768 100644 GIT binary patch delta 46 zcmV+}0MY-5z5|TG1F)u!lP-;lld4A!lUlP5lhBQ*lioWf2QC0206PE*lb??tv%HQ> EP~bQd6#xJL delta 18 acmeA<$lS4ydBd{4$qtjmHakr42><|5AP5)$ diff --git a/Resources/images/vr_off.png b/Resources/images/vr_off.png new file mode 100644 index 0000000000000000000000000000000000000000..5f319e5c8f05cab76695bddb373900429132a997 GIT binary patch literal 12846 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 b2YC4b%V(@KLQ0RH)d0%!8gf-K=E46D2OeY( literal 0 HcmV?d00001 diff --git a/Resources/images/vr_pause.png b/Resources/images/vr_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..af2d89acc98945412be133dafe955a9d62101730 GIT binary patch literal 6392 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 P-(Lr$wxL#?h7;yLPY<~f literal 0 HcmV?d00001 diff --git a/Resources/images/vr_play.png b/Resources/images/vr_play.png new file mode 100644 index 0000000000000000000000000000000000000000..6de2686f096bad08d5c6451422f224e47fc41458 GIT binary patch literal 7003 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 literal 0 HcmV?d00001 diff --git a/Resources/images/vr_wave.png b/Resources/images/vr_wave.png new file mode 100644 index 0000000000000000000000000000000000000000..26d7f1be77b3dd16a83756abd4bba75487a36c3e GIT binary patch literal 15224 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"; }; C61B1BF32667D202001A4E4A /* more_menu_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = more_menu_default.png; sourceTree = ""; }; C61B1BF62667EC6B001A4E4A /* ephemeral_messages_color_A.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ephemeral_messages_color_A.png; sourceTree = ""; }; + C622E3E926A8128F004F5434 /* vr_stop.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_stop.png; sourceTree = ""; }; + C622E3EA26A8128F004F5434 /* vr_wave.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_wave.png; sourceTree = ""; }; + C622E3EB26A8128F004F5434 /* vr_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_on.png; sourceTree = ""; }; + C622E3EC26A8128F004F5434 /* vr_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_off.png; sourceTree = ""; }; + C622E3ED26A8128F004F5434 /* vr_pause.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_pause.png; sourceTree = ""; }; + C622E3EE26A81290004F5434 /* vr_play.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_play.png; sourceTree = ""; }; C64A854C2667B66900252AD2 /* EphemeralSettingsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EphemeralSettingsView.h; sourceTree = ""; }; C64A854D2667B67200252AD2 /* EphemeralSettingsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EphemeralSettingsView.m; sourceTree = ""; }; C64A854F2667B67A00252AD2 /* EphemeralSettingsView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EphemeralSettingsView.xib; sourceTree = ""; }; @@ -1951,7 +1963,7 @@ files = ( 61DD7E1F2372E88F001BDD01 /* CoreLocation.framework in Frameworks */, 6180D6FE21EE41A800AD9CB6 /* QuickLook.framework in Frameworks */, - 61AEBEA321906AFC00F35E7F /* BuildFile in Frameworks */, + 61AEBEA321906AFC00F35E7F /* (null) in Frameworks */, D37DC7181594AF3400B2A5EB /* MessageUI.framework in Frameworks */, 61F1997520C6B1D5006B069A /* AVKit.framework in Frameworks */, 249660951FD6A35F001D55AA /* Photos.framework in Frameworks */, @@ -2285,7 +2297,7 @@ path = LinphoneUI; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( 8C23BCB71D82AAC3005F19BB /* linphone.entitlements */, @@ -2447,6 +2459,12 @@ 633FEBE11D3CD5570014B822 /* images */ = { isa = PBXGroup; children = ( + C622E3EC26A8128F004F5434 /* vr_off.png */, + C622E3EB26A8128F004F5434 /* vr_on.png */, + C622E3ED26A8128F004F5434 /* vr_pause.png */, + C622E3EE26A81290004F5434 /* vr_play.png */, + C622E3E926A8128F004F5434 /* vr_stop.png */, + C622E3EA26A8128F004F5434 /* vr_wave.png */, C61B1BF62667EC6B001A4E4A /* ephemeral_messages_color_A.png */, C61B1BF32667D202001A4E4A /* more_menu_default.png */, C61B1BF12667D075001A4E4A /* menu_security_default.png */, @@ -3357,7 +3375,7 @@ fr, hu, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -3408,6 +3426,7 @@ 615A2817217F280C0060F920 /* chat_list_indicator.png in Resources */, 633FEEFF1D3CD55A0014B822 /* options_add_call_disabled@2x.png in Resources */, 633FEF091D3CD55A0014B822 /* options_start_conference_disabled@2x.png in Resources */, + C622E3F326A81290004F5434 /* vr_pause.png in Resources */, 633FEE051D3CD5590014B822 /* cancel_edit_disabled@2x.png in Resources */, 633FEE5F1D3CD5590014B822 /* edit_list_default@2x.png in Resources */, 633FEEB61D3CD55A0014B822 /* numpad_3_over@2x.png in Resources */, @@ -3418,7 +3437,7 @@ 633FEED41D3CD55A0014B822 /* numpad_7_default@2x.png in Resources */, 633FEEE01D3CD55A0014B822 /* numpad_8_over~ipad@2x.png in Resources */, 633FEDDC1D3CD5590014B822 /* call_start_body_disabled~ipad.png in Resources */, - 63E802DB1C625AEF000D5509 /* BuildFile in Resources */, + 63E802DB1C625AEF000D5509 /* (null) in Resources */, 633FEE2E1D3CD5590014B822 /* color_F.png in Resources */, 633FEDC51D3CD5590014B822 /* call_hangup_disabled@2x.png in Resources */, 633FEEDF1D3CD55A0014B822 /* numpad_8_over~ipad.png in Resources */, @@ -3453,6 +3472,7 @@ 633FEDD91D3CD5590014B822 /* call_start_body_default~ipad@2x.png in Resources */, 633FEE401D3CD5590014B822 /* contacts_all_selected.png in Resources */, 633FEE0C1D3CD5590014B822 /* chat_attachment_disabled.png in Resources */, + C622E3EF26A81290004F5434 /* vr_stop.png in Resources */, 633FEF001D3CD55A0014B822 /* options_default.png in Resources */, CF15F21F20E4F9A3008B1DE6 /* UIImageViewDeletable.xib in Resources */, 633FEE951D3CD55A0014B822 /* micro_default@2x.png in Resources */, @@ -3478,6 +3498,7 @@ 633FEE7A1D3CD5590014B822 /* history_missed_default.png in Resources */, 633FEF121D3CD55A0014B822 /* pause_big_over_selected.png in Resources */, 633FED9D1D3CD5590014B822 /* add_field_default@2x.png in Resources */, + C622E3F426A81290004F5434 /* vr_play.png in Resources */, 639E9CB01C0DB83000019A75 /* SideMenuView.xib in Resources */, 633FEDBB1D3CD5590014B822 /* call_audio_start_default@2x.png in Resources */, 633FEF1A1D3CD55A0014B822 /* presence_away.png in Resources */, @@ -3505,6 +3526,7 @@ 615A283E2180A2560060F920 /* invite_linphone.png in Resources */, 633FEF281D3CD55A0014B822 /* route_earpiece_default.png in Resources */, 633FEE4F1D3CD5590014B822 /* delete_field_over@2x.png in Resources */, + C622E3F226A81290004F5434 /* vr_off.png in Resources */, 633FEE531D3CD5590014B822 /* dialer_alt_back@2x.png in Resources */, 633FEE3E1D3CD5590014B822 /* contacts_all_disabled.png in Resources */, 633FEEF31D3CD55A0014B822 /* numpad_over_background.png in Resources */, @@ -3740,6 +3762,7 @@ 633FEE191D3CD5590014B822 /* chat_send_over@2x.png in Resources */, 633FEF181D3CD55A0014B822 /* pause_small_over_selected.png in Resources */, 633FEE001D3CD5590014B822 /* camera_switch_over.png in Resources */, + C622E3F126A81290004F5434 /* vr_on.png in Resources */, 633FEF401D3CD55A0014B822 /* select_all_default.png in Resources */, 633FEDF01D3CD5590014B822 /* call_transfer_disabled.png in Resources */, 633FEE351D3CD5590014B822 /* conference_exit_default@2x.png in Resources */, @@ -3772,6 +3795,7 @@ 633FEEFA1D3CD55A0014B822 /* numpad_star~ipad.png in Resources */, D38187B915FE342200C3EDCA /* ContactDetailsView.xib in Resources */, 633FEE921D3CD55A0014B822 /* menu.png in Resources */, + C622E3F026A81290004F5434 /* vr_wave.png in Resources */, 633FEDE41D3CD5590014B822 /* call_status_incoming~ipad.png in Resources */, 633FEE4C1D3CD5590014B822 /* delete_field_default.png in Resources */, 633FEE391D3CD5590014B822 /* contact_add_default@2x.png in Resources */,