element-ios/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m

1405 lines
67 KiB
Objective-C

/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2017 Vector Creations Ltd
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "MXKRoomInputToolbarView.h"
#import "MXKSwiftHeader.h"
#import "MXKAppSettings.h"
@import MatrixSDK.MXMediaManager;
@import MediaPlayer;
@import MobileCoreServices;
@import Photos;
#import "MXKImageView.h"
#import "MXKTools.h"
#import "NSBundle+MatrixKit.h"
#import "MXKConstants.h"
@interface MXKRoomInputToolbarView()
{
/**
Alert used to list options.
*/
UIAlertController *optionsListView;
/**
Current media picker
*/
UIImagePickerController *mediaPicker;
/**
Array of validation views (MXKImageView instances)
*/
NSMutableArray *validationViews;
/**
Handle images attachment
*/
UIAlertController *compressionPrompt;
NSMutableArray *pendingImages;
}
@property (nonatomic) IBOutlet UIView *messageComposerContainer;
@end
@implementation MXKRoomInputToolbarView
@synthesize messageComposerContainer, inputAccessoryViewForKeyboard;
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKRoomInputToolbarView class])
bundle:[NSBundle bundleForClass:[MXKRoomInputToolbarView class]]];
}
+ (MXKRoomInputToolbarView *)instantiateRoomInputToolbarView
{
if ([[self class] nib])
{
return [[[self class] nib] instantiateWithOwner:nil options:nil].firstObject;
}
else
{
return [[self alloc] init];
}
}
- (void)awakeFromNib
{
[super awakeFromNib];
// Finalize setup
[self setTranslatesAutoresizingMaskIntoConstraints: NO];
// Disable send button
self.rightInputToolbarButton.enabled = NO;
// Enable text edition by default
self.editable = YES;
// Localize string
[_rightInputToolbarButton setTitle:[VectorL10n send] forState:UIControlStateNormal];
[_rightInputToolbarButton setTitle:[VectorL10n send] forState:UIControlStateHighlighted];
validationViews = [NSMutableArray array];
}
- (void)dealloc
{
inputAccessoryViewForKeyboard = nil;
[self destroy];
}
#pragma mark - Override MXKView
-(void)customizeViewRendering
{
[super customizeViewRendering];
// Reset default container background color
messageComposerContainer.backgroundColor = [UIColor clearColor];
// Set default toolbar background color
self.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
}
#pragma mark -
- (IBAction)onTouchUpInside:(UIButton*)button
{
if (button == self.leftInputToolbarButton)
{
if (optionsListView)
{
[optionsListView dismissViewControllerAnimated:NO completion:nil];
optionsListView = nil;
}
// Option button has been pressed
// List available options
__weak typeof(self) weakSelf = self;
// Check whether media attachment is supported
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:presentViewController:)])
{
optionsListView = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[optionsListView addAction:[UIAlertAction actionWithTitle:[VectorL10n attachMedia]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->optionsListView = nil;
// Open media gallery
self->mediaPicker = [[UIImagePickerController alloc] init];
self->mediaPicker.delegate = self;
self->mediaPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
self->mediaPicker.allowsEditing = NO;
self->mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil];
[self.delegate roomInputToolbarView:self presentViewController:self->mediaPicker];
}
}]];
[optionsListView addAction:[UIAlertAction actionWithTitle:[VectorL10n captureMedia]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->optionsListView = nil;
// Open Camera
self->mediaPicker = [[UIImagePickerController alloc] init];
self->mediaPicker.delegate = self;
self->mediaPicker.sourceType = UIImagePickerControllerSourceTypeCamera;
self->mediaPicker.allowsEditing = NO;
self->mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil];
[self.delegate roomInputToolbarView:self presentViewController:self->mediaPicker];
}
}]];
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] Attach media is not supported");
}
// Check whether user invitation is supported
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:inviteMatrixUser:)])
{
if (!optionsListView)
{
optionsListView = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
}
[optionsListView addAction:[UIAlertAction actionWithTitle:[VectorL10n inviteUser]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
// Ask for userId to invite
self->optionsListView = [UIAlertController alertControllerWithTitle:[VectorL10n userIdTitle] message:nil preferredStyle:UIAlertControllerStyleAlert];
[self->optionsListView addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->optionsListView = nil;
}
}]];
[self->optionsListView addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
textField.placeholder = [VectorL10n userIdPlaceholder];
}];
[self->optionsListView addAction:[UIAlertAction actionWithTitle:[VectorL10n invite] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
UITextField *textField = [self->optionsListView textFields].firstObject;
NSString *userId = textField.text;
self->optionsListView = nil;
if (userId.length)
{
[self.delegate roomInputToolbarView:self inviteMatrixUser:userId];
}
}
}]];
[self.delegate roomInputToolbarView:self presentAlertController:self->optionsListView];
}
}]];
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] Invitation is not supported");
}
if (optionsListView)
{
[self->optionsListView addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->optionsListView = nil;
}
}]];
[optionsListView popoverPresentationController].sourceView = button;
[optionsListView popoverPresentationController].sourceRect = button.bounds;
[self.delegate roomInputToolbarView:self presentAlertController:optionsListView];
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] No option is supported");
}
}
else if (button == self.rightInputToolbarButton && self.textMessage.length)
{
[self sendCurrentMessage];
}
}
- (void)sendCurrentMessage
{
// This forces an autocorrect event to happen when "Send" is pressed, which is necessary to accept a pending correction on send
self.textMessage = [NSString stringWithFormat:@"%@ ", self.textMessage];
self.textMessage = [self.textMessage substringToIndex:[self.textMessage length]-1];
NSString *message = self.textMessage;
// Reset message, disable view animation during the update to prevent placeholder distorsion.
[UIView setAnimationsEnabled:NO];
self.textMessage = nil;
[UIView setAnimationsEnabled:YES];
// Send button has been pressed
if (message.length && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendTextMessage:)])
{
[self.delegate roomInputToolbarView:self sendTextMessage:message];
}
}
- (void)setPlaceholder:(NSString *)inPlaceholder
{
_placeholder = inPlaceholder;
}
- (BOOL)becomeFirstResponder
{
return NO;
}
- (void)dismissKeyboard
{
}
- (void)dismissCompressionPrompt
{
if (compressionPrompt)
{
[compressionPrompt dismissViewControllerAnimated:NO completion:nil];
compressionPrompt = nil;
}
if (pendingImages.count)
{
NSData *firstImage = pendingImages.firstObject;
[pendingImages removeObjectAtIndex:0];
[self sendImage:firstImage withCompressionMode:MXKRoomInputToolbarCompressionModePrompt];
}
}
- (void)destroy
{
[self dismissValidationViews];
validationViews = nil;
if (optionsListView)
{
[optionsListView dismissViewControllerAnimated:NO completion:nil];
optionsListView = nil;
}
[self dismissMediaPicker];
self.delegate = nil;
pendingImages = nil;
[self dismissCompressionPrompt];
}
- (void)pasteText:(NSString *)text
{
// We cannot do more than appending text to self.textMessage
// Let 'MXKRoomInputToolbarView' children classes do the job
self.textMessage = [NSString stringWithFormat:@"%@%@", self.textMessage, text];
}
- (UIFont *)defaultFont
{
return [UIFont systemFontOfSize:15.f];
}
#pragma mark - MXKFileSizes
/**
Structure representing the file sizes of a media according to different level of
compression.
*/
typedef struct
{
NSUInteger small;
NSUInteger medium;
NSUInteger large;
NSUInteger original;
} MXKFileSizes;
void MXKFileSizes_init(MXKFileSizes *sizes)
{
memset(sizes, 0, sizeof(MXKFileSizes));
}
MXKFileSizes MXKFileSizes_add(MXKFileSizes sizes1, MXKFileSizes sizes2)
{
MXKFileSizes sizes;
sizes.small = sizes1.small + sizes2.small;
sizes.medium = sizes1.medium + sizes2.medium;
sizes.large = sizes1.large + sizes2.large;
sizes.original = sizes1.original + sizes2.original;
return sizes;
}
NSString* MXKFileSizes_description(MXKFileSizes sizes)
{
return [NSString stringWithFormat:@"small: %tu - medium: %tu - large: %tu - original: %tu", sizes.small, sizes.medium, sizes.large, sizes.original];
}
- (void)availableCompressionSizesForAsset:(PHAsset*)asset onComplete:(void(^)(MXKFileSizes sizes))onComplete
{
__block MXKFileSizes sizes;
MXKFileSizes_init(&sizes);
if (asset.mediaType == PHAssetMediaTypeImage)
{
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = NO;
options.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
if (imageData)
{
MXLogDebug(@"[MXKRoomInputToolbarView] availableCompressionSizesForAsset: Got image data");
UIImage *image = [UIImage imageWithData:imageData];
MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:image originalFileSize:imageData.length];
sizes.small = compressionSizes.small.fileSize;
sizes.medium = compressionSizes.medium.fileSize;
sizes.large = compressionSizes.large.fileSize;
sizes.original = compressionSizes.original.fileSize;
onComplete(sizes);
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] availableCompressionSizesForAsset: Failed to get image data");
// Notify user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
onComplete(sizes);
}
}];
}
else if (asset.mediaType == PHAssetMediaTypeVideo)
{
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]])
{
MXLogDebug(@"[MXKRoomInputToolbarView] availableCompressionSizesForAsset: Got video data");
AVURLAsset* urlAsset = (AVURLAsset*)asset;
NSNumber *size;
[urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil];
sizes.original = size.unsignedIntegerValue;
sizes.small = sizes.original;
sizes.medium = sizes.original;
sizes.large = sizes.original;
dispatch_async(dispatch_get_main_queue(), ^{
onComplete(sizes);
});
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] availableCompressionSizesForAsset: Failed to get video data");
// Notify user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
onComplete(sizes);
});
}
}];
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] availableCompressionSizesForAsset: unexpected media type");
onComplete(sizes);
}
}
- (void)availableCompressionSizesForAssets:(NSMutableArray<PHAsset*>*)checkedAssets index:(NSUInteger)index appendTo:(MXKFileSizes)sizes onComplete:(void(^)(NSArray<PHAsset*>*checkedAssets, MXKFileSizes fileSizes))onComplete
{
[self availableCompressionSizesForAsset:checkedAssets[index] onComplete:^(MXKFileSizes assetSizes) {
MXKFileSizes intermediateSizes;
NSUInteger nextIndex;
if (assetSizes.original == 0)
{
// Ignore this asset
[checkedAssets removeObjectAtIndex:index];
intermediateSizes = sizes;
nextIndex = index;
}
else
{
intermediateSizes = MXKFileSizes_add(sizes, assetSizes);
nextIndex = index + 1;
}
if (nextIndex == checkedAssets.count)
{
// Filter the sizes that are similar
if (intermediateSizes.medium >= intermediateSizes.large || intermediateSizes.large >= intermediateSizes.original)
{
intermediateSizes.large = 0;
}
if (intermediateSizes.small >= intermediateSizes.medium || intermediateSizes.medium >= intermediateSizes.original)
{
intermediateSizes.medium = 0;
}
if (intermediateSizes.small >= intermediateSizes.original)
{
intermediateSizes.small = 0;
}
onComplete(checkedAssets, intermediateSizes);
}
else
{
[self availableCompressionSizesForAssets:checkedAssets index:nextIndex appendTo:intermediateSizes onComplete:onComplete];
}
}];
}
- (void)availableCompressionSizesForAssets:(NSArray<PHAsset*>*)assets onComplete:(void(^)(NSArray<PHAsset*>*checkedAssets, MXKFileSizes fileSizes))onComplete
{
__block MXKFileSizes sizes;
MXKFileSizes_init(&sizes);
NSMutableArray<PHAsset*> *checkedAssets = [NSMutableArray arrayWithArray:assets];
[self availableCompressionSizesForAssets:checkedAssets index:0 appendTo:sizes onComplete:onComplete];
}
#pragma mark - Attachment handling
- (void)sendSelectedImage:(NSData*)imageData withMimeType:(NSString *)mimetype andCompressionMode:(MXKRoomInputToolbarCompressionMode)compressionMode isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
// Check condition before saving this media in user's library
if (_enableAutoSaving && !isPhotoLibraryAsset)
{
// Save the original image in user's photos library
UIImage *image = [UIImage imageWithData:imageData];
[MXMediaManager saveImageToPhotosLibrary:image success:nil failure:nil];
}
// Send data without compression if the image type is not jpeg
// Force compression for a heic image so that we generate jpeg from it
if (mimetype
&& [mimetype isEqualToString:@"image/jpeg"] == NO
&& [mimetype isEqualToString:@"image/heic"] == NO
&& [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendImage:withMimeType:)])
{
[self.delegate roomInputToolbarView:self sendImage:imageData withMimeType:mimetype];
}
else
{
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:sendImage:)])
{
[self sendImage:imageData withCompressionMode:compressionMode];
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] Attach image is not supported");
}
}
}
- (void)sendImage:(NSData*)imageData withCompressionMode:(MXKRoomInputToolbarCompressionMode)compressionMode
{
if (optionsListView)
{
[optionsListView dismissViewControllerAnimated:NO completion:nil];
optionsListView = nil;
}
if (compressionPrompt && compressionMode == MXKRoomInputToolbarCompressionModePrompt)
{
// Delay the image sending
if (!pendingImages)
{
pendingImages = [NSMutableArray arrayWithObject:imageData];
}
else
{
[pendingImages addObject:imageData];
}
return;
}
// Get available sizes for this image
UIImage *image = [UIImage imageWithData:imageData];
MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:image originalFileSize:imageData.length];
// Apply the compression mode
if (compressionMode == MXKRoomInputToolbarCompressionModePrompt
&& (compressionSizes.small.fileSize || compressionSizes.medium.fileSize || compressionSizes.large.fileSize))
{
__weak typeof(self) weakSelf = self;
compressionPrompt = [UIAlertController alertControllerWithTitle:[VectorL10n attachmentSizePromptTitle]
message:[VectorL10n attachmentSizePromptMessage]
preferredStyle:UIAlertControllerStyleActionSheet];
if (compressionSizes.small.fileSize)
{
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.small.fileSize];
NSString *title = [VectorL10n attachmentSmall:fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
// Send the small image
UIImage *smallImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE)];
[self.delegate roomInputToolbarView:self sendImage:smallImage];
[self dismissCompressionPrompt];
}
}]];
}
if (compressionSizes.medium.fileSize)
{
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.medium.fileSize];
NSString *title = [VectorL10n attachmentMedium:fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
// Send the medium image
UIImage *mediumImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE)];
[self.delegate roomInputToolbarView:self sendImage:mediumImage];
[self dismissCompressionPrompt];
}
}]];
}
if (compressionSizes.large.fileSize)
{
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.large.fileSize];
NSString *title = [VectorL10n attachmentLarge:fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
// Send the large image
UIImage *largeImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(compressionSizes.actualLargeSize, compressionSizes.actualLargeSize)];
[self.delegate roomInputToolbarView:self sendImage:largeImage];
[self dismissCompressionPrompt];
}
}]];
}
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize];
NSString *title = [VectorL10n attachmentOriginal:fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
// Send the original image
[self.delegate roomInputToolbarView:self sendImage:image];
[self dismissCompressionPrompt];
}
}]];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissCompressionPrompt];
}
}]];
[compressionPrompt popoverPresentationController].sourceView = self;
[compressionPrompt popoverPresentationController].sourceRect = self.bounds;
[self.delegate roomInputToolbarView:self presentAlertController:compressionPrompt];
}
else
{
// By default the original image is sent
UIImage *finalImage = image;
switch (compressionMode)
{
case MXKRoomInputToolbarCompressionModePrompt:
// Here the image size is too small to need compression - send the original image
break;
case MXKRoomInputToolbarCompressionModeSmall:
if (compressionSizes.small.fileSize)
{
finalImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE)];
}
break;
case MXKRoomInputToolbarCompressionModeMedium:
if (compressionSizes.medium.fileSize)
{
finalImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE)];
}
break;
case MXKRoomInputToolbarCompressionModeLarge:
if (compressionSizes.large.fileSize)
{
finalImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(compressionSizes.actualLargeSize, compressionSizes.actualLargeSize)];
}
break;
default:
// no compression, send original
break;
}
// Send the image
[self.delegate roomInputToolbarView:self sendImage:finalImage];
}
}
- (void)sendSelectedVideo:(NSURL*)selectedVideo isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
AVURLAsset *videoAsset = [AVURLAsset assetWithURL:selectedVideo];
[self sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
}
- (void)sendSelectedVideoAsset:(AVAsset*)selectedVideo isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
// Check condition before saving this media in user's library
if (_enableAutoSaving && !isPhotoLibraryAsset)
{
if ([selectedVideo isKindOfClass:[AVURLAsset class]])
{
AVURLAsset *urlAsset = (AVURLAsset*)selectedVideo;
[MXMediaManager saveMediaToPhotosLibrary:[urlAsset URL] isImage:NO success:nil failure:nil];
}
else
{
MXLogError(@"[RoomInputToolbarView] Unable to save video, incorrect asset type.")
}
}
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:sendVideoAsset:withThumbnail:)])
{
// Retrieve the video frame at 1 sec to define the video thumbnail
AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:selectedVideo];
assetImageGenerator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMake(1, 1);
CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil];
// Finalize video attachment
UIImage* videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CFRelease(imageRef);
[self.delegate roomInputToolbarView:self sendVideoAsset:selectedVideo withThumbnail:videoThumbnail];
}
else
{
MXLogDebug(@"[RoomInputToolbarView] Attach video is not supported");
}
}
- (void)sendSelectedAssets:(NSArray<PHAsset*>*)assets withCompressionMode:(MXKRoomInputToolbarCompressionMode)compressionMode
{
// Get data about the selected assets
if (assets.count)
{
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:updateActivityIndicator:)])
{
[self.delegate roomInputToolbarView:self updateActivityIndicator:YES];
}
[self availableCompressionSizesForAssets:assets onComplete:^(NSArray<PHAsset*>*checkedAssets, MXKFileSizes fileSizes) {
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:updateActivityIndicator:)])
{
[self.delegate roomInputToolbarView:self updateActivityIndicator:NO];
}
if (checkedAssets.count)
{
[self sendSelectedAssets:checkedAssets withFileSizes:fileSizes andCompressionMode:compressionMode];
}
}];
}
}
- (void)sendSelectedAssets:(NSArray<PHAsset*>*)assets withFileSizes:(MXKFileSizes)fileSizes andCompressionMode:(MXKRoomInputToolbarCompressionMode)compressionMode
{
if (compressionMode == MXKRoomInputToolbarCompressionModePrompt
&& (fileSizes.small || fileSizes.medium || fileSizes.large))
{
// Ask the user for the compression value
compressionPrompt = [UIAlertController alertControllerWithTitle:[VectorL10n attachmentSizePromptTitle]
message:[VectorL10n attachmentSizePromptMessage]
preferredStyle:UIAlertControllerStyleActionSheet];
__weak typeof(self) weakSelf = self;
if (fileSizes.small)
{
NSString *title = [VectorL10n attachmentSmall:[MXTools fileSizeToString:fileSizes.small]];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissCompressionPrompt];
[self sendSelectedAssets:assets withFileSizes:fileSizes andCompressionMode:MXKRoomInputToolbarCompressionModeSmall];
}
}]];
}
if (fileSizes.medium)
{
NSString *title = [VectorL10n attachmentMedium:[MXTools fileSizeToString:fileSizes.medium]];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissCompressionPrompt];
[self sendSelectedAssets:assets withFileSizes:fileSizes andCompressionMode:MXKRoomInputToolbarCompressionModeMedium];
}
}]];
}
if (fileSizes.large)
{
NSString *title = [VectorL10n attachmentLarge:[MXTools fileSizeToString:fileSizes.large]];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissCompressionPrompt];
[self sendSelectedAssets:assets withFileSizes:fileSizes andCompressionMode:MXKRoomInputToolbarCompressionModeLarge];
}
}]];
}
NSString *title = [VectorL10n attachmentOriginal:[MXTools fileSizeToString:fileSizes.original]];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissCompressionPrompt];
[self sendSelectedAssets:assets withFileSizes:fileSizes andCompressionMode:MXKRoomInputToolbarCompressionModeNone];
}
}]];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissCompressionPrompt];
}
}]];
[compressionPrompt popoverPresentationController].sourceView = self;
[compressionPrompt popoverPresentationController].sourceRect = self.bounds;
[self.delegate roomInputToolbarView:self presentAlertController:compressionPrompt];
}
else
{
// Send all media with the selected compression mode
for (PHAsset *asset in assets)
{
if (asset.mediaType == PHAssetMediaTypeImage)
{
// Retrieve the full sized image data
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = NO;
options.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
if (imageData)
{
MXLogDebug(@"[MXKRoomInputToolbarView] sendSelectedAssets: Got image data");
CFStringRef uti = (__bridge CFStringRef)dataUTI;
NSString *mimeType = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
[self sendSelectedImage:imageData withMimeType:mimeType andCompressionMode:compressionMode isPhotoLibraryAsset:YES];
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] sendSelectedAssets: Failed to get image data");
// Notify user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
}
}];
}
else if (asset.mediaType == PHAssetMediaTypeVideo)
{
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:[AVURLAsset class]])
{
MXLogDebug(@"[MXKRoomInputToolbarView] sendSelectedAssets: Got video data");
AVURLAsset* urlAsset = (AVURLAsset*)asset;
dispatch_async(dispatch_get_main_queue(), ^{
[self sendSelectedVideo:urlAsset.URL isPhotoLibraryAsset:YES];
});
}
else
{
MXLogDebug(@"[MXKRoomInputToolbarView] sendSelectedAssets: Failed to get video data");
// Notify user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
});
}
}];
}
}
}
}
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
[self dismissMediaPicker];
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage])
{
UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
if (selectedImage)
{
// Media picker does not offer a preview
// so add a preview to let the user validates his selection
if (picker.sourceType == UIImagePickerControllerSourceTypePhotoLibrary)
{
__weak typeof(self) weakSelf = self;
MXKImageView *imageValidationView = [[MXKImageView alloc] initWithFrame:CGRectZero];
imageValidationView.stretchable = YES;
// the user validates the image
[imageValidationView setRightButtonTitle:[VectorL10n ok] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
// Dismiss the image view
[self dismissValidationViews];
NSURL *imageLocalURL = [info objectForKey:UIImagePickerControllerReferenceURL];
if (imageLocalURL)
{
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[imageLocalURL.path pathExtension] , NULL);
NSString *mimetype = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
CFRelease(uti);
NSData *imageData = [NSData dataWithContentsOfFile:imageLocalURL.path];
// attach the selected image
[self sendSelectedImage:imageData withMimeType:mimetype andCompressionMode:MXKRoomInputToolbarCompressionModePrompt isPhotoLibraryAsset:YES];
}
}
}];
// the user wants to use an other image
[imageValidationView setLeftButtonTitle:[VectorL10n cancel] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
// dismiss the image view
[self dismissValidationViews];
// Open again media gallery
self->mediaPicker = [[UIImagePickerController alloc] init];
self->mediaPicker.delegate = self;
self->mediaPicker.sourceType = picker.sourceType;
self->mediaPicker.allowsEditing = NO;
self->mediaPicker.mediaTypes = picker.mediaTypes;
[self.delegate roomInputToolbarView:self presentViewController:self->mediaPicker];
}
}];
imageValidationView.image = selectedImage;
[validationViews addObject:imageValidationView];
[imageValidationView showFullScreen];
[self.delegate roomInputToolbarView:self hideStatusBar:YES];
}
else
{
// Suggest compression before sending image
NSData *imageData = UIImageJPEGRepresentation(selectedImage, 0.9);
[self sendSelectedImage:imageData withMimeType:nil andCompressionMode:MXKRoomInputToolbarCompressionModePrompt isPhotoLibraryAsset:NO];
}
}
}
else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie])
{
NSURL* selectedVideo = [info objectForKey:UIImagePickerControllerMediaURL];
[self sendSelectedVideo:selectedVideo isPhotoLibraryAsset:(picker.sourceType == UIImagePickerControllerSourceTypePhotoLibrary)];
}
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[self dismissMediaPicker];
}
- (void)dismissValidationViews
{
if (validationViews.count)
{
for (MXKImageView *validationView in validationViews)
{
[validationView dismissSelection];
[validationView removeFromSuperview];
}
[validationViews removeAllObjects];
// Restore status bar
[self.delegate roomInputToolbarView:self hideStatusBar:NO];
}
}
- (void)dismissValidationView:(MXKImageView*)validationView
{
[validationView dismissSelection];
[validationView removeFromSuperview];
if (validationViews.count)
{
[validationViews removeObject:validationView];
if (!validationViews.count)
{
// Restore status bar
[self.delegate roomInputToolbarView:self hideStatusBar:NO];
}
}
}
- (void)dismissMediaPicker
{
if (mediaPicker)
{
mediaPicker.delegate = nil;
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:dismissViewControllerAnimated:completion:)])
{
[self.delegate roomInputToolbarView:self dismissViewControllerAnimated:NO completion:^{
self->mediaPicker = nil;
}];
}
}
}
#pragma mark - Clipboard - Handle image/data paste from general pasteboard
- (void)paste:(id)sender
{
UIPasteboard *pasteboard = MXKPasteboardManager.shared.pasteboard;
if (pasteboard.numberOfItems)
{
[self dismissValidationViews];
[self dismissKeyboard];
__weak typeof(self) weakSelf = self;
for (NSDictionary* dict in pasteboard.items)
{
NSArray* allKeys = dict.allKeys;
for (NSString* key in allKeys)
{
NSString* MIMEType = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)key, kUTTagClassMIMEType);
if ([MIMEType hasPrefix:@"image/"] && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendImage:)])
{
UIImage *pasteboardImage;
if ([[dict objectForKey:key] isKindOfClass:UIImage.class])
{
pasteboardImage = [dict objectForKey:key];
}
// WebP images from Safari appear on the pasteboard as NSData rather than UIImages.
else if ([[dict objectForKey:key] isKindOfClass:NSData.class])
{
pasteboardImage = [UIImage imageWithData:[dict objectForKey:key]];
}
else {
NSString *message = [NSString stringWithFormat:@"[MXKRoomInputToolbarView] Unsupported image format %@ for mimetype %@ pasted.", MIMEType, NSStringFromClass([[dict objectForKey:key] class])];
MXLogError(message);
}
if (pasteboardImage)
{
MXKImageView *imageValidationView = [[MXKImageView alloc] initWithFrame:CGRectZero];
imageValidationView.stretchable = YES;
// the user validates the image
[imageValidationView setRightButtonTitle:[VectorL10n ok] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissValidationView:imageView];
[self.delegate roomInputToolbarView:self sendImage:pasteboardImage];
}
}];
// the user wants to use an other image
[imageValidationView setLeftButtonTitle:[VectorL10n cancel] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
// Dismiss the image validation view.
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissValidationView:imageView];
}
}];
imageValidationView.image = pasteboardImage;
[validationViews addObject:imageValidationView];
[imageValidationView showFullScreen];
[self.delegate roomInputToolbarView:self hideStatusBar:YES];
}
break;
}
else if ([MIMEType hasPrefix:@"video/"] && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendVideo:withThumbnail:)])
{
NSData *pasteboardVideoData = [dict objectForKey:key];
// Get a unique cache path to store this video
NSString *cacheFilePath = [MXMediaManager temporaryCachePathInFolder:nil withType:MIMEType];
if ([MXMediaManager writeMediaData:pasteboardVideoData toFilePath:cacheFilePath])
{
NSURL *videoLocalURL = [NSURL fileURLWithPath:cacheFilePath isDirectory:NO];
// Retrieve the video frame at 1 sec to define the video thumbnail
AVURLAsset *urlAsset = [[AVURLAsset alloc] initWithURL:videoLocalURL options:nil];
AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:urlAsset];
assetImageGenerator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMake(1, 1);
CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil];
UIImage* videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CFRelease (imageRef);
MXKImageView *videoValidationView = [[MXKImageView alloc] initWithFrame:CGRectZero];
videoValidationView.stretchable = YES;
// the user validates the image
[videoValidationView setRightButtonTitle:[VectorL10n ok] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissValidationView:imageView];
[self.delegate roomInputToolbarView:self sendVideo:videoLocalURL withThumbnail:videoThumbnail];
}
}];
// the user wants to use an other image
[videoValidationView setLeftButtonTitle:[VectorL10n cancel] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
// Dismiss the video validation view.
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissValidationView:imageView];
}
}];
videoValidationView.image = videoThumbnail;
[validationViews addObject:videoValidationView];
[videoValidationView showFullScreen];
[self.delegate roomInputToolbarView:self hideStatusBar:YES];
// Add video icon
UIImageView *videoIconView = [[UIImageView alloc] initWithImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_video"]];
videoIconView.center = videoValidationView.center;
videoIconView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
[videoValidationView addSubview:videoIconView];
}
break;
}
else if ([MIMEType hasPrefix:@"application/"] && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendFile:withMimeType:)])
{
NSData *pasteboardDocumentData = [dict objectForKey:key];
// Get a unique cache path to store this data
NSString *cacheFilePath = [MXMediaManager temporaryCachePathInFolder:nil withType:MIMEType];
if ([MXMediaManager writeMediaData:pasteboardDocumentData toFilePath:cacheFilePath])
{
NSURL *localURL = [NSURL fileURLWithPath:cacheFilePath isDirectory:NO];
MXKImageView *docValidationView = [[MXKImageView alloc] initWithFrame:CGRectZero];
docValidationView.stretchable = YES;
// the user validates the image
[docValidationView setRightButtonTitle:[VectorL10n ok] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissValidationView:imageView];
[self.delegate roomInputToolbarView:self sendFile:localURL withMimeType:MIMEType];
}
}];
// the user wants to use an other image
[docValidationView setLeftButtonTitle:[VectorL10n cancel] handler:^(MXKImageView* imageView, NSString* buttonTitle)
{
// Dismiss the validation view.
if (weakSelf)
{
typeof(self) self = weakSelf;
[self dismissValidationView:imageView];
}
}];
docValidationView.image = nil;
[validationViews addObject:docValidationView];
[docValidationView showFullScreen];
[self.delegate roomInputToolbarView:self hideStatusBar:YES];
// Create a fake name based on fileData to keep the same name for the same file.
NSString *dataHash = [pasteboardDocumentData mx_MD5];
if (dataHash.length > 7)
{
// Crop
dataHash = [dataHash substringToIndex:7];
}
NSString *extension = [MXTools fileExtensionFromContentType:MIMEType];
NSString *filename = [NSString stringWithFormat:@"file_%@%@", dataHash, extension];
// Display this file name
UITextView *fileNameTextView = [[UITextView alloc] initWithFrame:CGRectZero];
fileNameTextView.text = filename;
fileNameTextView.font = [UIFont systemFontOfSize:17];
[fileNameTextView sizeToFit];
fileNameTextView.center = docValidationView.center;
fileNameTextView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
docValidationView.backgroundColor = [UIColor whiteColor];
[docValidationView addSubview:fileNameTextView];
}
break;
}
}
}
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(paste:) && MXKAppSettings.standardAppSettings.messageDetailsAllowPastingMedia)
{
// Check whether some data listed in general pasteboard can be paste
UIPasteboard *pasteboard = MXKPasteboardManager.shared.pasteboard;
if (pasteboard.numberOfItems)
{
for (NSArray<NSString *> *types in [pasteboard pasteboardTypesForItemSet:nil])
{
for (NSString *type in types)
{
NSString* MIMEType = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)type, kUTTagClassMIMEType);
if ([MIMEType hasPrefix:@"image/"] && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendImage:)])
{
return YES;
}
if ([MIMEType hasPrefix:@"video/"] && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendVideo:withThumbnail:)])
{
return YES;
}
if ([MIMEType hasPrefix:@"application/"] && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendFile:withMimeType:)])
{
return YES;
}
}
}
}
}
return NO;
}
- (void)setPartialContent:(NSAttributedString *)attributedTextMessage
{
self.attributedTextMessage = attributedTextMessage;
}
@end