diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a779fd05..4a93306c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [Unreleased] + +### Added +- Auto-layout of images in chat messages + +### Changed +- Use of Photokit instead of Asset Library for image handling + +### Fixed + +### Removed + ## [4.0] - 2018-06-11 ### Added diff --git a/Classes/ChatConversationTableView.h b/Classes/ChatConversationTableView.h index 0f6162f47..cce625051 100644 --- a/Classes/ChatConversationTableView.h +++ b/Classes/ChatConversationTableView.h @@ -27,7 +27,7 @@ @protocol ChatConversationDelegate -- (BOOL)startImageUpload:(UIImage *)image url:(NSURL *)url withQuality:(float)quality; +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality; - (BOOL)startFileUpload:(NSData *)data withUrl:(NSURL *)url; - (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url; - (void)tableViewIsScrolling; @@ -41,6 +41,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 b3f9de5ff..4a9f924fc 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]; @@ -231,7 +237,6 @@ static const CGFloat MESSAGE_SPACING_PERCENTAGE = 5.f; if (nextEvent) { LinphoneChatMessage *nextChat = linphone_event_log_get_chat_message(nextEvent); if (!linphone_address_equal(linphone_chat_message_get_from_address(nextChat), linphone_chat_message_get_from_address(chat))) { - LOGD(@"BITE"); height += tableView.frame.size.height * MESSAGE_SPACING_PERCENTAGE / 100; } } diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index bf5dd6555..54077999b 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" @@ -220,7 +222,7 @@ static UICompositeViewDescription *compositeDescription = nil; //share photo NSData *data = dict[@"nsData"]; UIImage *image = [[UIImage alloc] initWithData:data]; - [self chooseImageQuality:image url:nil]; + [self chooseImageQuality:image assetId:nil]; [defaults removeObjectForKey:@"img"]; } else if (dictWeb) { //share url, if local file, then upload file @@ -308,38 +310,11 @@ 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{ + [self startImageUpload:image assetId:phAssetId withQuality:quality]; } -- (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 +324,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]; @@ -601,9 +576,9 @@ 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; } @@ -621,7 +596,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 +610,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 { 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..575963902 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,38 @@ 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]; - } + PHAsset *phasset = [info objectForKey:UIImagePickerControllerPHAsset]; + 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 +245,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/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 485fde3d9..c0c078c10 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -263,7 +263,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 +287,7 @@ struct codec_name_pref_table codec_pref_table[] = {{"speex", 8000, "speex_8k_pre } [self migrateFromUserPrefs]; + [self loadAvatar]; } return self; } @@ -2887,20 +2887,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 @@ -3077,4 +3076,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..ec2e12142 100644 --- a/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib +++ b/Classes/LinphoneUI/Base.lproj/UIChatBubblePhotoCell.xib @@ -1,5 +1,5 @@ - + @@ -21,6 +21,7 @@ + @@ -35,19 +36,19 @@ - + - + - + - + @@ -55,7 +56,7 @@ -