diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f60a379..b9956e1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,18 @@ Group changes to describe their impact on the project, as follows: ## [Unreleased] +### Added +- Auto-layout of images in chat messages +- Selection of multiple images to send in a chat message +- Sending text with image +- Latest Calls widget +- Latest Chatrooms widget +- Homescreen quick action : New message +- Rich message notifications with Linphone UI +- Support of H265 video format based on Apple's VideoToolbox framework. + +### Changed +- Use of Photokit instead of Asset Library for image handling ### Fixed - Fix Bluetooth management diff --git a/Classes/Base.lproj/ChatConversationView.xib b/Classes/Base.lproj/ChatConversationView.xib index 604c03dc1..90c5815cd 100644 --- a/Classes/Base.lproj/ChatConversationView.xib +++ b/Classes/Base.lproj/ChatConversationView.xib @@ -18,6 +18,8 @@ + + @@ -269,6 +271,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Classes/ChatConversationTableView.h b/Classes/ChatConversationTableView.h index 0f6162f47..a29288367 100644 --- a/Classes/ChatConversationTableView.h +++ b/Classes/ChatConversationTableView.h @@ -27,8 +27,9 @@ @protocol ChatConversationDelegate -- (BOOL)startImageUpload:(UIImage *)image url:(NSURL *)url withQuality:(float)quality; -- (BOOL)startFileUpload:(NSData *)data withUrl:(NSURL *)url; +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality; +- (BOOL)startFileUpload:(NSData *)data assetId:(NSString *)phAssetId; +- (BOOL)startFileUpload:(NSData *)data withName:(NSString *)name; - (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url; - (void)tableViewIsScrolling; @@ -41,6 +42,7 @@ @property(nonatomic) LinphoneChatRoom *chatRoom; @property(nonatomic, strong) id chatRoomDelegate; +@property NSMutableDictionary *imagesInChatroom; - (void)addEventEntry:(LinphoneEventLog *)event; - (void)scrollToBottom:(BOOL)animated; diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m index 0ff816e0d..20842d192 100644 --- a/Classes/ChatConversationTableView.m +++ b/Classes/ChatConversationTableView.m @@ -39,29 +39,36 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.tableView.accessibilityIdentifier = @"ChatRoom list"; + _imagesInChatroom = [NSMutableDictionary dictionary]; } #pragma mark - - (void)clearEventList { - [eventList removeAllObjects]; + for (NSValue *value in eventList) { + LinphoneEventLog *event = value.pointerValue; + linphone_event_log_unref(event); + } + [eventList removeAllObjects]; } - (void)updateData { + [self clearEventList]; if (!_chatRoom) return; - [self clearEventList]; + LinphoneChatRoomCapabilitiesMask capabilities = linphone_chat_room_get_capabilities(_chatRoom); bctbx_list_t *chatRoomEvents = (capabilities & LinphoneChatRoomCapabilitiesOneToOne) ? linphone_chat_room_get_history_message_events(_chatRoom, 0) : linphone_chat_room_get_history_events(_chatRoom, 0); - + bctbx_list_t *head = chatRoomEvents; eventList = [[NSMutableArray alloc] initWithCapacity:bctbx_list_size(chatRoomEvents)]; while (chatRoomEvents) { LinphoneEventLog *event = (LinphoneEventLog *)chatRoomEvents->data; [eventList addObject:[NSValue valueWithPointer:linphone_event_log_ref(event)]]; chatRoomEvents = chatRoomEvents->next; } + bctbx_list_free_with_data(head, (bctbx_list_free_func)linphone_event_log_unref); for (FileTransferDelegate *ftd in [LinphoneManager.instance fileTransferDelegates]) { const LinphoneAddress *ftd_peer = @@ -83,7 +90,6 @@ - (void)addEventEntry:(LinphoneEventLog *)event { [eventList addObject:[NSValue valueWithPointer:linphone_event_log_ref(event)]]; int pos = (int)eventList.count - 1; - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:pos inSection:0]; [self.tableView beginUpdates]; [self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationFade]; @@ -98,16 +104,16 @@ } [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:FALSE]; // just reload - return; + return; } - (void)scrollToBottom:(BOOL)animated { - [self.tableView reloadData]; + //[self.tableView reloadData]; size_t count = eventList.count; if (!count) return; - [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0]]; + //[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0]]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES]; diff --git a/Classes/ChatConversationView.h b/Classes/ChatConversationView.h index 7a264ca8e..3fa69519f 100644 --- a/Classes/ChatConversationView.h +++ b/Classes/ChatConversationView.h @@ -27,12 +27,13 @@ #import "UIRoundedImageView.h" #import "UIBackToCallButton.h" #import "Utils/HPGrowingTextView/HPGrowingTextView.h" +#import "UIImageViewDeletable.h" #include "linphone/linphonecore.h" @interface ChatConversationView : TPMultiLayoutViewController { + UIDocumentInteractionControllerDelegate, UISearchBarDelegate, UIImageViewDeletableDelegate, UICollectionViewDataSource> { OrderedDictionary *imageQualities; BOOL scrollOnGrowingEnabled; BOOL composingVisible; @@ -60,6 +61,12 @@ @property (weak, nonatomic) IBOutlet UIIconButton *infoButton; @property (weak, nonatomic) IBOutlet UILabel *particpantsLabel; @property (nonatomic, strong) UIDocumentInteractionController *documentInteractionController; +@property NSMutableArray *imagesArray; +@property NSMutableArray *assetIdsArray; +@property NSMutableArray *qualitySettingsArray; +@property (weak, nonatomic) IBOutlet UICollectionView *imagesCollectionView; +@property (weak, nonatomic) IBOutlet UIView *imagesView; + + (void)markAsRead:(LinphoneChatRoom *)chatRoom; - (void)configureForRoom:(BOOL)editing; @@ -73,6 +80,7 @@ - (IBAction)onDeleteClick:(id)sender; - (IBAction)onEditionChangeClick:(id)sender; - (void)update; -- (void)openResults:(NSString *) filePath; +- (void)openFile:(NSString *) filePath; +- (void)clearMessageView; @end diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index bf5dd6555..840cc3d7f 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -17,6 +17,8 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#import + #import "ChatConversationView.h" #import "PhoneMainView.h" #import "Utils.h" @@ -99,6 +101,8 @@ static UICompositeViewDescription *compositeDescription = nil; _messageField.contentInset = UIEdgeInsetsMake(-15, 0, 0, 0); // _messageField.internalTextView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, 0, 10); [_tableController setChatRoomDelegate:self]; + [_imagesCollectionView registerClass:[UIImageViewDeletable class] forCellWithReuseIdentifier:NSStringFromClass([UIImageViewDeletable class])]; + [_imagesCollectionView setDataSource:self]; } - (void)viewWillAppear:(BOOL)animated { @@ -123,6 +127,24 @@ static UICompositeViewDescription *compositeDescription = nil; selector:@selector(callUpdateEvent:) name:kLinphoneCallUpdate object:nil]; + + if ([_imagesArray count] > 0) { + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - 100; + imagesFrame.size.height = 100; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height -= 100; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:nil]; + } } - (void)viewWillDisappear:(BOOL)animated { @@ -151,6 +173,7 @@ static UICompositeViewDescription *compositeDescription = nil; [self configureForRoom:true]; _backButton.hidden = _tableController.isEditing; [_tableController scrollToBottom:true]; + [self refreshImageDrawer]; } #pragma mark - @@ -201,7 +224,6 @@ static UICompositeViewDescription *compositeDescription = nil; _messageField.editable = !linphone_chat_room_has_been_left(_chatRoom); _pictureButton.enabled = !linphone_chat_room_has_been_left(_chatRoom); _messageView.userInteractionEnabled = !linphone_chat_room_has_been_left(_chatRoom); - [_messageField setText:@""]; [_tableController setChatRoom:_chatRoom]; _chatView.hidden = NO; @@ -211,38 +233,34 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)shareFile { NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:groupName]; - - NSDictionary *dict = [defaults valueForKey:@"img"]; - NSDictionary *dictWeb = [defaults valueForKey:@"web"]; - NSDictionary *dictFile = [defaults valueForKey:@"mov"]; - NSDictionary *dictText = [defaults valueForKey:@"text"]; + NSDictionary *dict = [defaults valueForKey:@"photoData"]; + NSDictionary *dictFile = [defaults valueForKey:@"icloudData"]; + NSDictionary *dictUrl = [defaults valueForKey:@"url"]; if (dict) { - //share photo - NSData *data = dict[@"nsData"]; - UIImage *image = [[UIImage alloc] initWithData:data]; - [self chooseImageQuality:image url:nil]; - [defaults removeObjectForKey:@"img"]; - } else if (dictWeb) { - //share url, if local file, then upload file - NSString *url = dictWeb[@"url"]; - NSURL *fileUrl = [NSURL fileURLWithPath:url]; - if ([url hasPrefix:@"file"]) { - //local file - NSData *data = dictWeb[@"nsData"]; - [self confirmShare:data url:fileUrl text:nil]; + //file shared from photo lib + NSString *fileName = dict[@"url"]; + NSString *key = [[fileName componentsSeparatedByString:@"."] firstObject]; + NSMutableDictionary * assetDict = [LinphoneUtils photoAssetsDictionary]; + if ([fileName hasSuffix:@"JPG"] || [fileName hasSuffix:@"PNG"]) { + UIImage *image = [[UIImage alloc] initWithData:dict[@"nsData"]]; + [self chooseImageQuality:image assetId:[[assetDict objectForKey:key] localIdentifier]]; + } else if ([fileName hasSuffix:@"MOV"]) { + [self confirmShare:dict[@"nsData"] url:nil fileName:nil assetId:[[assetDict objectForKey:key] localIdentifier]]; } else { - [self confirmShare:nil url:nil text:url]; + LOGE(@"Unable to parse file %@",fileName); } - [defaults removeObjectForKey:@"web"]; - }else if (dictFile) { - //share file - NSData *data = dictFile[@"nsData"]; - [self confirmShare:data url:[NSURL fileURLWithPath:dictFile[@"url"]] text:nil]; - [defaults removeObjectForKey:@"mov"]; - }else if (dictText) { - //share text - [self confirmShare:nil url:nil text:dictText[@"name"]]; - [defaults removeObjectForKey:@"text"]; + + [defaults removeObjectForKey:@"photoData"]; + } else if (dictFile) { + NSString *fileName = dictFile[@"url"]; + [self confirmShare:dictFile[@"nsData"] url:nil fileName:fileName assetId:nil]; + + [defaults removeObjectForKey:@"icloudData"]; + } else if (dictUrl) { + NSString *url = dictUrl[@"url"]; + [self confirmShare:nil url:url fileName:nil assetId:nil]; + + [defaults removeObjectForKey:@"url"]; } } @@ -308,38 +326,15 @@ static UICompositeViewDescription *compositeDescription = nil; return TRUE; } -- (void)saveAndSend:(UIImage *)image url:(NSURL *)url withQuality:(float)quality{ - // photo from Camera, must be saved first - if (url == nil) { - [LinphoneManager.instance.photoLibrary - writeImageToSavedPhotosAlbum:image.CGImage - orientation:(ALAssetOrientation)[image imageOrientation] - completionBlock:^(NSURL *assetURL, NSError *error) { - if (error) { - LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); - - UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Transfer error", nil) - message:NSLocalizedString(@"Cannot write image to photo library", - nil) - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) {}]; - - [errView addAction:defaultAction]; - [self presentViewController:errView animated:YES completion:nil]; - } else { - LOGI(@"Image saved to [%@]", [assetURL absoluteString]); - [self startImageUpload:image url:assetURL withQuality:quality]; - } - }]; - } else { - [self startImageUpload:image url:url withQuality:quality]; - } +- (void)saveAndSend:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality{ + + [_imagesArray addObject:image]; + [_assetIdsArray addObject:phAssetId]; + [_qualitySettingsArray addObject:@(quality)]; + [self refreshImageDrawer]; } -- (void)chooseImageQuality:(UIImage *)image url:(NSURL *)url { +- (void)chooseImageQuality:(UIImage *)image assetId:(NSString *)phAssetId { DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Choose the image size", nil)]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (NSString *key in [imageQualities allKeys]) { @@ -349,7 +344,7 @@ static UICompositeViewDescription *compositeDescription = nil; NSString *text = [NSString stringWithFormat:@"%@ (%@)", key, [size toHumanReadableSize]]; [sheet addButtonWithTitle:text block:^() { - [self saveAndSend:image url:url withQuality:[quality floatValue]]; + [self saveAndSend:image assetId:phAssetId withQuality:[quality floatValue]]; }]; } [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; @@ -359,17 +354,17 @@ static UICompositeViewDescription *compositeDescription = nil; }); } -- (void)confirmShare:(NSData *)data url:(NSURL *)url text:(NSString *)text { +- (void)confirmShare:(NSData *)data url:(NSString *)url fileName:(NSString *)fileName assetId:(NSString *)phAssetId { DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"", nil)]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [sheet addButtonWithTitle:@"send to this friend" block:^() { - if(data && url) - [self startFileUpload:data withUrl:url]; + if (url) + [self sendMessage:url withExterlBodyUrl:nil withInternalURL:nil]; + else if (fileName) + [self startFileUpload:data withName:fileName]; else - [self sendMessage:text withExterlBodyUrl:nil withInternalURL:nil]; - + [self startFileUpload:data assetId:phAssetId]; }]; [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; @@ -481,6 +476,12 @@ static UICompositeViewDescription *compositeDescription = nil; messageRect.size.height += diff; [_messageView setFrame:messageRect]; + if ([_imagesArray count] > 0) { + CGRect _imagesRect = [_imagesView frame]; + _imagesRect.origin.y -= diff; + [_imagesView setFrame:_imagesRect]; + } + // Always stay at bottom if (scrollOnGrowingEnabled) { CGRect tableFrame = [_tableController.view frame]; @@ -518,6 +519,15 @@ static UICompositeViewDescription *compositeDescription = nil; } - (IBAction)onSendClick:(id)event { + if ([_imagesArray count] > 0) { + int i = 0; + for (i = 0; i < [_imagesArray count] - 1; ++i) { + [self startImageUpload:[_imagesArray objectAtIndex:i] assetId:[_assetIdsArray objectAtIndex:i] withQuality:[_qualitySettingsArray objectAtIndex:i].floatValue]; + } + [self startImageUpload:[_imagesArray objectAtIndex:i] assetId:[_assetIdsArray objectAtIndex:i] withQuality:[_qualitySettingsArray objectAtIndex:i].floatValue andMessage:[self.messageField text]]; + [self clearMessageView]; + return; + } if ([self sendMessage:[_messageField text] withExterlBodyUrl:nil withInternalURL:nil]) { scrollOnGrowingEnabled = FALSE; [_messageField setText:@""]; @@ -601,16 +611,31 @@ static UICompositeViewDescription *compositeDescription = nil; #pragma mark ChatRoomDelegate -- (BOOL)startImageUpload:(UIImage *)image url:(NSURL *)url withQuality:(float)quality { +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; - [fileTransfer upload:image withURL:url forChatRoom:_chatRoom withQuality:quality]; + [fileTransfer upload:image withassetId:phAssetId forChatRoom:_chatRoom withQuality:quality]; [_tableController scrollToBottom:true]; return TRUE; } -- (BOOL)startFileUpload:(NSData *)data withUrl:(NSURL *)url { +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality andMessage:(NSString *)message { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; - [fileTransfer uploadFile:data forChatRoom:_chatRoom withUrl:url]; + [fileTransfer setText:message]; + [fileTransfer upload:image withassetId:phAssetId forChatRoom:_chatRoom withQuality:quality]; + [_tableController scrollToBottom:true]; + return TRUE; +} + +- (BOOL)startFileUpload:(NSData *)data assetId:(NSString *)phAssetId { + FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; + [fileTransfer uploadVideo:data withassetId:phAssetId forChatRoom:_chatRoom]; + [_tableController scrollToBottom:true]; + return TRUE; +} + +- (BOOL)startFileUpload:(NSData *)data withName:(NSString *)name { + FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; + [fileTransfer uploadFile:data forChatRoom:_chatRoom withName:name]; [_tableController scrollToBottom:true]; return TRUE; } @@ -621,7 +646,7 @@ static UICompositeViewDescription *compositeDescription = nil; #pragma mark ImagePickerDelegate -- (void)imagePickerDelegateImage:(UIImage *)image info:(NSDictionary *)info { +- (void)imagePickerDelegateImage:(UIImage *)image info:(NSString *)phAssetId { // When getting image from the camera, it may be 90° rotated due to orientation // (image.imageOrientation = UIImageOrientationRight). Just rotate it to be face up. if (image.imageOrientation != UIImageOrientationUp) { @@ -635,9 +660,7 @@ static UICompositeViewDescription *compositeDescription = nil; if (IPAD) { [VIEW(ImagePickerView).popoverController dismissPopoverAnimated:TRUE]; } - - NSURL *url = [info valueForKey:UIImagePickerControllerReferenceURL]; - [self chooseImageQuality:image url:url]; + [self chooseImageQuality:image assetId:phAssetId]; } - (void)tableViewIsScrolling { @@ -651,6 +674,9 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)keyboardWillHide:(NSNotification *)notif { NSTimeInterval duration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + int heightDiff = UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) ? 55 : 105; + [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState @@ -694,6 +720,19 @@ static UICompositeViewDescription *compositeDescription = nil; } } } + + + if ([_imagesArray count] > 0){ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - heightDiff; + imagesFrame.size.height = heightDiff; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; + [_tableController.tableView setFrame:tableViewFrame]; + } } completion:^(BOOL finished){ @@ -702,7 +741,9 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)keyboardWillShow:(NSNotification *)notif { NSTimeInterval duration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - + + int heightDiff = UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) ? 55 : 105; + [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState @@ -749,6 +790,18 @@ static UICompositeViewDescription *compositeDescription = nil; [_messageView frame].origin.y - tableFrame.origin.y - composeIndicatorCompensation; [_tableController.view setFrame:tableFrame]; } + + if ([_imagesArray count] > 0){ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - heightDiff; + imagesFrame.size.height = heightDiff; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; + [_tableController.tableView setFrame:tableViewFrame]; + } // Scroll NSInteger lastSection = [_tableController.tableView numberOfSections] - 1; @@ -761,8 +814,10 @@ static UICompositeViewDescription *compositeDescription = nil; animated:FALSE]; } } + } completion:^(BOOL finished){ + }]; } @@ -852,7 +907,7 @@ void on_chat_room_conference_left(LinphoneChatRoom *cr, const LinphoneEventLog * [view.tableController scrollToBottom:true]; } -- (void)openResults:(NSString *) filePath +- (void)openFile:(NSString *) filePath { // Open the controller. _documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:filePath]]; @@ -866,4 +921,80 @@ void on_chat_room_conference_left(LinphoneChatRoom *cr, const LinphoneEventLog * } } +- (void)deleteImageWithAssetId:(NSString *)assetId { + NSUInteger key = [_assetIdsArray indexOfObject:assetId]; + [_imagesArray removeObjectAtIndex:key]; + [_assetIdsArray removeObjectAtIndex:key]; + [self refreshImageDrawer]; +} + +- (void)clearMessageView { + [_messageField setText:@""]; + _imagesArray = [NSMutableArray array]; + _assetIdsArray = [NSMutableArray array]; + + [self refreshImageDrawer]; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return [_imagesArray count]; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + UIImageViewDeletable *imgView = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([UIImageViewDeletable class]) forIndexPath:indexPath]; + CGRect imgFrame = imgView.frame; + imgFrame.origin.y = 5; + if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) { + imgFrame.size.height = 50; + } else { + imgFrame.size.height = 100; + } + [imgView.image setImage:[UIImage resizeImage:[_imagesArray objectAtIndex:[indexPath item]] withMaxWidth:imgFrame.size.width andMaxHeight:imgFrame.size.height]]; + [imgView setAssetId:[_assetIdsArray objectAtIndex:[indexPath item]]]; + [imgView setDeleteDelegate:self]; + [imgView setFrame:imgFrame]; + [_sendButton setEnabled:TRUE]; + return imgView; +} + +- (void)refreshImageDrawer { + int heightDiff = UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) ? 55 : 105; + + if ([_imagesArray count] == 0) { + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y; + imagesFrame.size.height = 0; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:nil]; + if ([_messageField.text isEqualToString:@""]) + [_sendButton setEnabled:FALSE]; + } else { + [UIView animateWithDuration:0 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + // resizing imagesView + CGRect imagesFrame = [_imagesView frame]; + imagesFrame.origin.y = [_messageView frame].origin.y - heightDiff; + imagesFrame.size.height = heightDiff; + [_imagesView setFrame:imagesFrame]; + // resizing chatTable + CGRect tableViewFrame = [_tableController.tableView frame]; + tableViewFrame.size.height = imagesFrame.origin.y - tableViewFrame.origin.y; + [_tableController.tableView setFrame:tableViewFrame]; + } + completion:^(BOOL result){[_imagesCollectionView reloadData];}]; + } +} + @end diff --git a/Classes/ChatsListTableView.m b/Classes/ChatsListTableView.m index 4264087b9..ad78cb768 100644 --- a/Classes/ChatsListTableView.m +++ b/Classes/ChatsListTableView.m @@ -141,6 +141,10 @@ static int sorted_history_comparison(LinphoneChatRoom *to_insert, LinphoneChatRo while (sorted) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; LinphoneChatRoom *cr = sorted->data; + if (!cr) { + sorted = sorted->next; + continue; + } const LinphoneAddress *peer_address = linphone_chat_room_get_peer_address(cr); const LinphoneAddress *local_address = linphone_chat_room_get_local_address(cr); NSString *display; @@ -148,9 +152,17 @@ static int sorted_history_comparison(LinphoneChatRoom *to_insert, LinphoneChatRo forKey:@"peer"]; [dict setObject:[NSString stringWithUTF8String:linphone_address_as_string_uri_only(local_address)] forKey:@"local"]; - if (linphone_chat_room_get_conference_address(cr)) + if (linphone_chat_room_get_conference_address(cr)) { + if (!linphone_chat_room_get_subject(cr)) { + sorted = sorted->next; + continue; + } display = [NSString stringWithUTF8String:linphone_chat_room_get_subject(cr)]; - else { + } else { + if (!linphone_address_get_username(peer_address)) { + sorted = sorted->next; + continue; + } display = [NSString stringWithUTF8String:linphone_address_get_display_name(peer_address)?:linphone_address_get_username(peer_address)]; if ([FastAddressBook imageForAddress:peer_address]) [dict setObject:UIImageJPEGRepresentation([UIImage resizeImage:[FastAddressBook imageForAddress:peer_address] @@ -161,7 +173,7 @@ static int sorted_history_comparison(LinphoneChatRoom *to_insert, LinphoneChatRo } [dict setObject:display forKey:@"display"]; - [dict setObject:[NSNumber numberWithBool:linphone_chat_room_get_conference_address(cr)] + [dict setObject:[NSNumber numberWithBool:!!linphone_chat_room_get_conference_address(cr)] forKey:@"nbParticipants"]; [addresses addObject:dict]; if (addresses.count >= 4) //send no more data than needed diff --git a/Classes/ImagePickerView.h b/Classes/ImagePickerView.h index 1f03bb5fd..bc02c7a15 100644 --- a/Classes/ImagePickerView.h +++ b/Classes/ImagePickerView.h @@ -21,7 +21,7 @@ @protocol ImagePickerDelegate -- (void)imagePickerDelegateImage:(UIImage *)image info:(NSDictionary *)info; +- (void)imagePickerDelegateImage:(UIImage *)image info:(NSString *)phAssetId; @end diff --git a/Classes/ImagePickerView.m b/Classes/ImagePickerView.m index c5ee92daf..f5a0cc62f 100644 --- a/Classes/ImagePickerView.m +++ b/Classes/ImagePickerView.m @@ -20,7 +20,6 @@ #import #import #import -#import #import "ImagePickerView.h" #import "PhoneMainView.h" @@ -162,15 +161,46 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { [self dismiss]; - UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage]; - if (image == nil) { - image = [info objectForKey:UIImagePickerControllerOriginalImage]; - } - if (image != nil && imagePickerDelegate != nil) { - [imagePickerDelegate imagePickerDelegateImage:image info:info]; - } + + NSURL *alassetURL = [info objectForKey:UIImagePickerControllerReferenceURL]; + PHAsset *phasset = nil; + // when photo from camera, it hasn't be saved + if (alassetURL) { + PHFetchResult *phFetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[alassetURL] options:nil]; + phasset = [phFetchResult firstObject]; + } + + UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage] ? [info objectForKey:UIImagePickerControllerEditedImage] : [info objectForKey:UIImagePickerControllerOriginalImage]; + if (!phasset) { + __block PHObjectPlaceholder *placeHolder; + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAssetFromImage:image]; + placeHolder = [request placeholderForCreatedAsset]; + } completionHandler:^(BOOL success, NSError *error) { + if (success) { + LOGI(@"Image saved to [%@]", [placeHolder localIdentifier]); + [self passImageToDelegate:image PHAssetId:[placeHolder localIdentifier]]; + } else { + LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); + } + } + ]; + return; + } + [self passImageToDelegate:image PHAssetId:[phasset localIdentifier]]; } +- (void) passImageToDelegate:(UIImage *)image PHAssetId:(NSString *)assetId { + if (imagePickerDelegate != nil) { + [imagePickerDelegate imagePickerDelegateImage:image info:(NSString *)assetId]; + } +} +/* + if (imagePickerDelegate != nil) { + [imagePickerDelegate imagePickerDelegateImage:image info:(__bridge NSDictionary *)contextInfo]; + } +} +*/ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [self dismiss]; } @@ -223,35 +253,57 @@ static UICompositeViewDescription *compositeDescription = nil; [PhoneMainView.instance changeCurrentView:view.compositeViewDescription]; } }; - - DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Select the source", nil)]; - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - [sheet addButtonWithTitle:NSLocalizedString(@"Camera", nil) - block:^() { - if([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized ){ - if([PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusDenied ){ - block(UIImagePickerControllerSourceTypeCamera); - }else{ - [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; - } - }else { - [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Camera's permission", nil) message:NSLocalizedString(@"Camera not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; - } - }]; - } - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { - [sheet addButtonWithTitle:NSLocalizedString(@"Photo library", nil) - block:^() { - if([PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusDenied ){ - block(UIImagePickerControllerSourceTypePhotoLibrary); - }else{ - [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; - } - }]; - } - [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; - - [sheet showInView:PhoneMainView.instance.view]; + if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) { + DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Select the source", nil)]; + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Camera", nil) + block:^() { + if([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] != AVAuthorizationStatusAuthorized ) { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Camera's permission", nil) message:NSLocalizedString(@"Camera not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + return; + } + block(UIImagePickerControllerSourceTypeCamera); + }]; + } + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Photo library", nil) + block:^() { + block(UIImagePickerControllerSourceTypePhotoLibrary); + }]; + } + [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; + + [sheet showInView:PhoneMainView.instance.view]; + } else { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) { + DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Select the source", nil)]; + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Camera", nil) + block:^() { + if([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] != AVAuthorizationStatusAuthorized ) { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Camera's permission", nil) message:NSLocalizedString(@"Camera not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + return; + } + block(UIImagePickerControllerSourceTypeCamera); + }]; + } + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Photo library", nil) + block:^() { + block(UIImagePickerControllerSourceTypePhotoLibrary); + }]; + } + [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; + + [sheet showInView:PhoneMainView.instance.view]; + } else { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + } + }); + }]; + } } @end diff --git a/Classes/LinphoneAppDelegate.m b/Classes/LinphoneAppDelegate.m index f48979be3..ca5863cff 100644 --- a/Classes/LinphoneAppDelegate.m +++ b/Classes/LinphoneAppDelegate.m @@ -685,6 +685,9 @@ userInfo:sheet repeats:NO]; } else if ([response.notification.request.content.categoryIdentifier isEqual:@"zrtp_request"]) { + if (!call) + return; + NSString *code = [NSString stringWithUTF8String:linphone_call_get_authentication_token(call)]; NSString *myCode; NSString *correspondantCode; diff --git a/Classes/LinphoneManager.h b/Classes/LinphoneManager.h index c1eab0464..9dd0454de 100644 --- a/Classes/LinphoneManager.h +++ b/Classes/LinphoneManager.h @@ -20,7 +20,7 @@ #import #import #import -#import +#import #import #import @@ -208,6 +208,8 @@ typedef struct _LinphoneManagerSounds { - (void)checkNewVersion; +- (void)loadAvatar; + @property ProviderDelegate *providerDelegate; @property (readonly) BOOL isTesting; @@ -225,7 +227,6 @@ typedef struct _LinphoneManagerSounds { @property (nonatomic, assign) BOOL speakerEnabled; @property (nonatomic, assign) BOOL bluetoothAvailable; @property (nonatomic, assign) BOOL bluetoothEnabled; -@property (readonly) ALAssetsLibrary *photoLibrary; @property (readonly) NSString* contactSipField; @property (readonly,copy) NSString* contactFilter; @property (copy) void (^silentPushCompletion)(UIBackgroundFetchResult); @@ -238,5 +239,6 @@ typedef struct _LinphoneManagerSounds { @property NSDictionary *pushDict; @property(strong, nonatomic) OrderedDictionary *linphoneManagerAddressBookMap; @property (nonatomic, assign) BOOL contactsUpdated; +@property UIImage *avatar; @end diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index c5cdfcdc7..bb6365458 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -136,6 +136,7 @@ struct codec_name_pref_table codec_pref_table[] = {{"speex", 8000, "speex_8k_pre {"g729", 8000, "g729_preference"}, {"mp4v-es", 90000, "mp4v-es_preference"}, {"h264", 90000, "h264_preference"}, + {"h265", 90000, "h265_preference"}, {"vp8", 90000, "vp8_preference"}, {"mpeg4-generic", 16000, "aaceld_16k_preference"}, {"mpeg4-generic", 22050, "aaceld_22k_preference"}, @@ -263,7 +264,6 @@ struct codec_name_pref_table codec_pref_table[] = {{"speex", 8000, "speex_8k_pre _fileTransferDelegates = [[NSMutableArray alloc] init]; _linphoneManagerAddressBookMap = [[OrderedDictionary alloc] init]; pushCallIDs = [[NSMutableArray alloc] init]; - _photoLibrary = [[ALAssetsLibrary alloc] init]; _isTesting = [LinphoneManager isRunningTests]; [self renameDefaultSettings]; [self copyDefaultSettings]; @@ -288,6 +288,7 @@ struct codec_name_pref_table codec_pref_table[] = {{"speex", 8000, "speex_8k_pre } [self migrateFromUserPrefs]; + [self loadAvatar]; } return self; } @@ -1230,7 +1231,39 @@ static void linphone_iphone_popup_password_request(LinphoneCore *lc, LinphoneAut } content.sound = [UNNotificationSound soundNamed:@"msg.caf"]; content.categoryIdentifier = @"msg_cat"; - content.userInfo = @{@"from" : from, @"peer_addr" : peer_uri, @"local_addr" : local_uri, @"CallId" : callID}; + // save data to user info for rich notification content + NSMutableArray *msgs = [NSMutableArray array]; + bctbx_list_t *history = linphone_chat_room_get_history(room, 6); + while (history) { + NSMutableDictionary *msgData = [NSMutableDictionary dictionary]; + LinphoneChatMessage *msg = history->data; + const char *state = linphone_chat_message_state_to_string(linphone_chat_message_get_state(msg)); + bool_t isOutgoing = linphone_chat_message_is_outgoing(msg); + bool_t isFileTransfer = (linphone_chat_message_get_file_transfer_information(msg) != NULL); + const LinphoneAddress *fromAddress = linphone_chat_message_get_from_address(msg); + NSString *displayNameDate = [NSString stringWithFormat:@"%@ - %@", [LinphoneUtils timeToString:linphone_chat_message_get_time(msg) + withFormat:LinphoneDateChatBubble], + [FastAddressBook displayNameForAddress:fromAddress]]; + UIImage *fromImage = [UIImage resizeImage:[FastAddressBook imageForAddress:fromAddress] + withMaxWidth:200 + andMaxHeight:200]; + NSData *fromImageData = UIImageJPEGRepresentation(fromImage, 1); + [msgData setObject:[NSString stringWithUTF8String:state] forKey:@"state"]; + [msgData setObject:displayNameDate forKey:@"displayNameDate"]; + [msgData setObject:[NSNumber numberWithBool:isFileTransfer] forKey:@"isFileTransfer"]; + [msgData setObject:fromImageData forKey:@"fromImageData"]; + if (isFileTransfer) { + LinphoneContent *file = linphone_chat_message_get_file_transfer_information(msg); + const char *filename = linphone_content_get_name(file); + [msgData setObject:[NSString stringWithUTF8String:filename] forKey:@"msg"]; + } else { + [msgData setObject:[UIChatBubbleTextCell TextMessageForChat:msg] forKey:@"msg"]; + } + [msgData setObject:[NSNumber numberWithBool:isOutgoing] forKey:@"isOutgoing"]; + [msgs addObject:msgData]; + history = bctbx_list_next(history); + } + content.userInfo = @{@"from" : from, @"peer_addr" : peer_uri, @"local_addr" : local_uri, @"CallId" : callID, @"msgs" : msgs}; content.accessibilityLabel = @"Message notif"; UNNotificationRequest *req = [UNNotificationRequest requestWithIdentifier:@"call_request" content:content trigger:NULL]; [[UNUserNotificationCenter currentNotificationCenter] @@ -2482,7 +2515,7 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { bool allow = true; AVAudioSessionRouteDescription *newRoute = [AVAudioSession sharedInstance].currentRoute; - if (newRoute) { + if (newRoute && newRoute.outputs.count > 0) { NSString *route = newRoute.outputs[0].portType; allow = !([route isEqualToString:AVAudioSessionPortLineOut] || [route isEqualToString:AVAudioSessionPortHeadphones] || @@ -2505,7 +2538,7 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { AVAudioSessionRouteDescription *newRoute = [AVAudioSession sharedInstance].currentRoute; - if (newRoute) { + if (newRoute && newRoute.outputs.count > 0) { NSString *route = newRoute.outputs[0].portType; LOGI(@"Current audio route is [%s]", [route UTF8String]); @@ -2585,6 +2618,7 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { linphone_call_params_enable_video(lcallParams, video); linphone_call_accept_with_params(call, lcallParams); + linphone_call_params_unref(lcallParams); } - (void)send:(NSString *)replyText toChatRoom:(LinphoneChatRoom *)room { @@ -2886,20 +2920,19 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { } + (void)setValueInMessageAppData:(id)value forKey:(NSString *)key inMessage:(LinphoneChatMessage *)msg { + NSMutableDictionary *appDataDict = [NSMutableDictionary dictionary]; + const char *appData = linphone_chat_message_get_appdata(msg); + if (appData) { + appDataDict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithBytes:appData length:strlen(appData)] + options:NSJSONReadingMutableContainers + error:nil]; + } - NSMutableDictionary *appDataDict = [NSMutableDictionary dictionary]; - const char *appData = linphone_chat_message_get_appdata(msg); - if (appData) { - appDataDict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithBytes:appData length:strlen(appData)] - options:NSJSONReadingMutableContainers - error:nil]; - } + [appDataDict setValue:value forKey:key]; - [appDataDict setValue:value forKey:key]; - - NSData *data = [NSJSONSerialization dataWithJSONObject:appDataDict options:0 error:nil]; - NSString *appdataJSON = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - linphone_chat_message_set_appdata(msg, [appdataJSON UTF8String]); + NSData *data = [NSJSONSerialization dataWithJSONObject:appDataDict options:0 error:nil]; + NSString *appdataJSON = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + linphone_chat_message_set_appdata(msg, [appdataJSON UTF8String]); } #pragma mark - LPConfig Functions @@ -3076,4 +3109,33 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { const char *curVersionCString = [curVersion cStringUsingEncoding:NSUTF8StringEncoding]; linphone_core_check_for_update(theLinphoneCore, curVersionCString); } + +- (void)loadAvatar { + NSString *assetId = [self lpConfigStringForKey:@"avatar"]; + __block UIImage *ret = nil; + if (assetId) { + PHFetchResult *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetId] options:nil]; + if (![assets firstObject]) { + LOGE(@"Can't fetch avatar image."); + } + PHAsset *asset = [assets firstObject]; + // load avatar synchronously so that we can return UIIMage* directly - since we are + // only using thumbnail, it must be pretty fast to fetch even without cache. + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = TRUE; + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options + resultHandler:^(UIImage *image, NSDictionary * info) { + if (image) + ret = [UIImage UIImageThumbnail:image thumbSize:150]; + else + LOGE(@"Can't read avatar"); + }]; + } + + if (!ret) { + ret = [UIImage imageNamed:@"avatar.png"]; + } + _avatar = ret; +} + @end diff --git a/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib b/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib index 65e3aef60..c453ff689 100644 --- a/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib +++ b/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib @@ -1,5 +1,5 @@ - + @@ -19,15 +19,18 @@ - + + + + - + @@ -35,19 +38,19 @@ - - + + - - + + - - + + - + @@ -55,7 +58,7 @@ - - +