element-ios/Riot/Modules/Room/Attachements/MXKAttachmentsViewController.m

1484 lines
60 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 "MXKAttachmentsViewController.h"
#import <WebKit/WebKit.h>
@import MatrixSDK.MXMediaManager;
#import "MXKMediaCollectionViewCell.h"
#import "MXKPieChartView.h"
#import "MXKConstants.h"
#import "MXKTools.h"
#import "NSBundle+MatrixKit.h"
#import "MXKEventFormatter.h"
#import "MXKAttachmentInteractionController.h"
#import "MXKSwiftHeader.h"
#import "LegacyAppDelegate.h"
@interface MXKAttachmentsViewController () <UINavigationControllerDelegate, UIViewControllerTransitioningDelegate>
{
/**
Current alert (if any).
*/
UIAlertController *currentAlert;
/**
Navigation bar handling
*/
NSTimer *navigationBarDisplayTimer;
/**
SplitViewController handling
*/
BOOL shouldRestoreBottomBar;
UISplitViewControllerDisplayMode savedSplitViewControllerDisplayMode;
/**
Audio session handling
*/
NSString *savedAVAudioSessionCategory;
/**
The attachments array (MXAttachment instances).
*/
NSMutableArray *attachments;
/**
The index of the current visible collection item
*/
NSInteger currentVisibleItemIndex;
/**
The document interaction Controller used to share attachment
*/
UIDocumentInteractionController *documentInteractionController;
MXKAttachment *currentSharedAttachment;
/**
Tells whether back pagination is in progress.
*/
BOOL isBackPaginationInProgress;
/**
A temporary file used to store decrypted attachments
*/
NSString *tempFile;
/**
Path to a file containing video data for the currently selected
attachment, if it's a video attachment and the data is
available.
*/
NSString *videoFile;
}
//animations
@property (nonatomic) MXKAttachmentInteractionController *interactionController;
@property (nonatomic, weak) UIViewController <MXKSourceAttachmentAnimatorDelegate> *sourceViewController;
@property (nonatomic) UIImageView *originalImageView;
@property (nonatomic) CGRect convertedFrame;
@property (nonatomic) BOOL customAnimationsEnabled;
@property (nonatomic) BOOL isLoadingVideo;
@end
@implementation MXKAttachmentsViewController
@synthesize attachments;
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKAttachmentsViewController class])
bundle:[NSBundle bundleForClass:[MXKAttachmentsViewController class]]];
}
+ (instancetype)attachmentsViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKAttachmentsViewController class])
bundle:[NSBundle bundleForClass:[MXKAttachmentsViewController class]]];
}
+ (instancetype)animatedAttachmentsViewControllerWithSourceViewController:(UIViewController <MXKSourceAttachmentAnimatorDelegate> *)sourceViewController
{
MXKAttachmentsViewController *attachmentsController = [[[self class] alloc] initWithNibName:NSStringFromClass([MXKAttachmentsViewController class])
bundle:[NSBundle bundleForClass:[MXKAttachmentsViewController class]]];
//create an interactionController for it to handle the gestue recognizer and control the interactions
attachmentsController.interactionController = [[MXKAttachmentInteractionController alloc] initWithDestinationViewController:attachmentsController sourceViewController:sourceViewController];
//we use the animationsEnabled property to enable/disable animations. Instances created not using this method should use the default animations
attachmentsController.customAnimationsEnabled = YES;
//this properties will be needed by animationControllers in order to perform the animations
attachmentsController.sourceViewController = sourceViewController;
//setting transitioningDelegate and navigationController.delegate so that the animations will work for present/dismiss as well as push/pop
attachmentsController.transitioningDelegate = attachmentsController;
sourceViewController.navigationController.delegate = attachmentsController;
return attachmentsController;
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
tempFile = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_attachmentsCollection)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
self.backButton.image = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"back_icon"];
// Register collection view cell class
[self.attachmentsCollection registerClass:MXKMediaCollectionViewCell.class forCellWithReuseIdentifier:[MXKMediaCollectionViewCell defaultReuseIdentifier]];
// Hide collection to hide first scrolling into the attachments.
_attachmentsCollection.hidden = YES;
// Display collection cell in full screen
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
self.automaticallyAdjustsScrollViewInsets = NO;
#pragma clang diagnostic pop
}
- (BOOL)prefersStatusBarHidden
{
// Hide status bar.
// Caution: Enable [UIViewController prefersStatusBarHidden] use at application level
// by turning on UIViewControllerBasedStatusBarAppearance in Info.plist.
return YES;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
videoFile = nil;
savedAVAudioSessionCategory = [[AVAudioSession sharedInstance] category];
// Hide navigation bar by default.
[self hideNavigationBar];
// Hide status bar
// TODO: remove this [UIApplication statusBarHidden] use (deprecated since iOS 9).
// Note: setting statusBarHidden does nothing if your application is using the default UIViewController-based status bar system.
UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
if (sharedApplication)
{
sharedApplication.statusBarHidden = YES;
}
// Handle here the case of splitviewcontroller use on iOS 8 and later.
if (self.splitViewController && [self.splitViewController respondsToSelector:@selector(displayMode)])
{
if (self.hidesBottomBarWhenPushed)
{
// This screen should be displayed without tabbar, but hidesBottomBarWhenPushed flag has no effect in case of splitviewcontroller use.
// Trick: on iOS 8 and later the tabbar is hidden manually
shouldRestoreBottomBar = YES;
self.tabBarController.tabBar.hidden = YES;
}
// Hide the primary view controller to allow full screen display
savedSplitViewControllerDisplayMode = [self.splitViewController displayMode];
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden;
[self.splitViewController.view layoutIfNeeded];
}
[_attachmentsCollection reloadData];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Adjust content offset and make visible the attachmnet collections
[self refreshAttachmentCollectionContentOffset];
_attachmentsCollection.hidden = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
if (tempFile)
{
NSError *err;
[[NSFileManager defaultManager] removeItemAtPath:tempFile error:&err];
tempFile = nil;
}
if (currentAlert)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
currentAlert = nil;
}
// Stop playing any video
for (MXKMediaCollectionViewCell *cell in self.attachmentsCollection.visibleCells)
{
[cell.moviePlayer.player pause];
cell.moviePlayer.player = nil;
}
// Restore audio category
if (savedAVAudioSessionCategory)
{
[[AVAudioSession sharedInstance] setCategory:savedAVAudioSessionCategory error:nil];
savedAVAudioSessionCategory = nil;
}
[navigationBarDisplayTimer invalidate];
navigationBarDisplayTimer = nil;
// Restore status bar
// TODO: remove this [UIApplication statusBarHidden] use (deprecated since iOS 9).
// Note: setting statusBarHidden does nothing if your application is using the default UIViewController-based status bar system.
UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
if (sharedApplication)
{
sharedApplication.statusBarHidden = NO;
}
if (shouldRestoreBottomBar)
{
self.tabBarController.tabBar.hidden = NO;
}
if (self.splitViewController && [self.splitViewController respondsToSelector:@selector(displayMode)])
{
self.splitViewController.preferredDisplayMode = savedSplitViewControllerDisplayMode;
[self.splitViewController.view layoutIfNeeded];
}
[super viewWillDisappear:animated];
}
- (void)dealloc
{
[self destroy];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(coordinator.transitionDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// Cell width will be updated, force collection layout refresh to take into account the changes
[self->_attachmentsCollection.collectionViewLayout invalidateLayout];
// Refresh the current attachment display
[self refreshAttachmentCollectionContentOffset];
});
}
#pragma mark - Override MXKViewController
- (void)destroy
{
if (documentInteractionController)
{
[documentInteractionController dismissPreviewAnimated:NO];
[documentInteractionController dismissMenuAnimated:NO];
documentInteractionController = nil;
}
if (currentSharedAttachment)
{
[currentSharedAttachment onShareEnded];
currentSharedAttachment = nil;
}
if (self.sourceViewController)
{
self.sourceViewController.navigationController.delegate = nil;
self.sourceViewController = nil;
}
[super destroy];
}
#pragma mark - Public API
- (void)displayAttachments:(NSArray*)attachmentArray focusOn:(NSString*)eventId
{
if ([attachmentArray isEqualToArray:attachments] && eventId.length == 0)
{
// neither the attachments nor the focus changed, can be ignored
return;
}
NSString *currentAttachmentEventId = eventId;
NSString *currentAttachmentOriginalFileName = nil;
if (currentAttachmentEventId.length == 0 && attachments)
{
if (isBackPaginationInProgress && currentVisibleItemIndex == 0)
{
// Here the spinner were displayed, we update the viewer by displaying the first added attachment
// (the one just added before the first item of the current attachments array).
if (attachments.count)
{
// Retrieve the event id of the first item in the current attachments array
MXKAttachment *attachment = attachments[0];
NSString *firstAttachmentEventId = attachment.eventId;
NSString *firstAttachmentOriginalFileName = nil;
// The original file name is used when the attachment is a local echo.
// Indeed its event id may be replaced by the actual one in the new attachments array.
if ([firstAttachmentEventId hasPrefix:kMXEventLocalEventIdPrefix])
{
firstAttachmentOriginalFileName = attachment.originalFileName;
}
// Look for the attachment added before this attachment in new array.
for (attachment in attachmentArray)
{
if (firstAttachmentOriginalFileName && [attachment.originalFileName isEqualToString:firstAttachmentOriginalFileName])
{
break;
}
else if ([attachment.eventId isEqualToString:firstAttachmentEventId])
{
break;
}
currentAttachmentEventId = attachment.eventId;
}
}
}
else if (currentVisibleItemIndex != NSNotFound)
{
// Compute the attachment index
NSUInteger currentAttachmentIndex = (isBackPaginationInProgress ? currentVisibleItemIndex - 1 : currentVisibleItemIndex);
if (currentAttachmentIndex < attachments.count)
{
MXKAttachment *attachment = attachments[currentAttachmentIndex];
currentAttachmentEventId = attachment.eventId;
// The original file name is used when the attachment is a local echo.
// Indeed its event id may be replaced by the actual one in the new attachments array.
if ([currentAttachmentEventId hasPrefix:kMXEventLocalEventIdPrefix])
{
currentAttachmentOriginalFileName = attachment.originalFileName;
}
}
}
}
// Stop back pagination (Do not call here 'stopBackPaginationActivity' because a full collection reload is planned at the end).
isBackPaginationInProgress = NO;
// Set/reset the attachments array
attachments = [NSMutableArray arrayWithArray:attachmentArray];
// Update the index of the current displayed attachment by looking for the
// current event id (or the current original file name, if any) in the new attachments array.
currentVisibleItemIndex = 0;
if (currentAttachmentEventId)
{
for (NSUInteger index = 0; index < attachments.count; index++)
{
MXKAttachment *attachment = attachments[index];
// Check first the original filename if any.
if (currentAttachmentOriginalFileName && [attachment.originalFileName isEqualToString:currentAttachmentOriginalFileName])
{
currentVisibleItemIndex = index;
break;
}
// Check the event id then
else if ([attachment.eventId isEqualToString:currentAttachmentEventId])
{
currentVisibleItemIndex = index;
break;
}
}
}
// Refresh
[_attachmentsCollection reloadData];
// Adjust content offset
[self refreshAttachmentCollectionContentOffset];
}
- (void)setComplete:(BOOL)complete
{
_complete = complete;
if (complete)
{
[self stopBackPaginationActivity];
}
}
- (IBAction)onButtonPressed:(id)sender
{
if (sender == self.backButton)
{
[self withdrawViewControllerAnimated:YES completion:nil];
}
}
#pragma mark - Privates
- (IBAction)hideNavigationBar
{
self.navigationBarContainer.hidden = YES;
[navigationBarDisplayTimer invalidate];
navigationBarDisplayTimer = nil;
}
- (void)refreshCurrentVisibleItemIndex
{
// Check whether the collection is actually rendered
if (_attachmentsCollection.contentSize.width)
{
// Get the window from the app delegate as this can be called before the view is presented.
UIWindow *window = LegacyAppDelegate.theDelegate.window;
currentVisibleItemIndex = _attachmentsCollection.contentOffset.x / window.bounds.size.width;
}
else
{
currentVisibleItemIndex = NSNotFound;
}
}
- (void)refreshAttachmentCollectionContentOffset
{
if (currentVisibleItemIndex != NSNotFound && _attachmentsCollection)
{
// Get the window from the app delegate as this can be called before the view is presented.
UIWindow *window = LegacyAppDelegate.theDelegate.window;
// Set the content offset to display the current attachment
CGPoint contentOffset = _attachmentsCollection.contentOffset;
contentOffset.x = currentVisibleItemIndex * window.bounds.size.width;
_attachmentsCollection.contentOffset = contentOffset;
}
}
- (void)refreshCurrentVisibleCell
{
// In case of attached image, load here the high res image.
[self refreshCurrentVisibleItemIndex];
if (currentVisibleItemIndex == NSNotFound) {
// Tell the delegate that no attachment is displayed for the moment
if ([self.delegate respondsToSelector:@selector(displayedNewAttachmentWithEventId:)])
{
[self.delegate displayedNewAttachmentWithEventId:nil];
}
}
else
{
NSInteger item = currentVisibleItemIndex;
if (isBackPaginationInProgress)
{
if (item == 0)
{
// Tell the delegate that no attachment is displayed for the moment
if ([self.delegate respondsToSelector:@selector(displayedNewAttachmentWithEventId:)])
{
[self.delegate displayedNewAttachmentWithEventId:nil];
}
return;
}
item --;
}
if (item < attachments.count)
{
MXKAttachment *attachment = attachments[item];
NSString *mimeType = attachment.contentInfo[@"mimetype"];
// Tell the delegate which attachment has been shown using its eventId
if ([self.delegate respondsToSelector:@selector(displayedNewAttachmentWithEventId:)])
{
[self.delegate displayedNewAttachmentWithEventId:attachment.eventId];
}
// Check attachment type
if (attachment.type == MXKAttachmentTypeImage && attachment.contentURL && ![mimeType isEqualToString:@"image/gif"])
{
// Retrieve the related cell
UICollectionViewCell *cell = [_attachmentsCollection cellForItemAtIndexPath:[NSIndexPath indexPathForItem:currentVisibleItemIndex inSection:0]];
if ([cell isKindOfClass:[MXKMediaCollectionViewCell class]])
{
MXKMediaCollectionViewCell *mediaCollectionViewCell = (MXKMediaCollectionViewCell*)cell;
// Load high res image
mediaCollectionViewCell.mxkImageView.stretchable = YES;
mediaCollectionViewCell.mxkImageView.enableInMemoryCache = NO;
[mediaCollectionViewCell.mxkImageView setAttachment:attachment];
}
}
}
}
}
- (void)stopBackPaginationActivity
{
if (isBackPaginationInProgress)
{
isBackPaginationInProgress = NO;
[self.attachmentsCollection deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]]];
}
}
- (void)prepareVideoForItem:(NSInteger)item success:(void(^)(void))success failure:(void(^)(NSError *))failure
{
MXKAttachment *attachment = attachments[item];
if (attachment.isEncrypted)
{
[attachment decryptToTempFile:^(NSString *file) {
if (self->tempFile)
{
[[NSFileManager defaultManager] removeItemAtPath:self->tempFile error:nil];
}
self->tempFile = file;
self->videoFile = file;
success();
} failure:^(NSError *error) {
if (failure) failure(error);
}];
}
else
{
if ([[NSFileManager defaultManager] fileExistsAtPath:attachment.cacheFilePath])
{
videoFile = attachment.cacheFilePath;
success();
}
else
{
[attachment prepare:^{
self->videoFile = attachment.cacheFilePath;
success();
} failure:^(NSError *error) {
if (failure) failure(error);
}];
}
}
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
if (isBackPaginationInProgress)
{
return (attachments.count + 1);
}
return attachments.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MXKMediaCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MXKMediaCollectionViewCell defaultReuseIdentifier]
forIndexPath:indexPath];
NSInteger item = indexPath.item;
if (isBackPaginationInProgress)
{
if (item == 0)
{
cell.mxkImageView.hidden = YES;
cell.customView.hidden = NO;
// Add back pagination spinner
UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
spinner.hidesWhenStopped = NO;
spinner.backgroundColor = [UIColor clearColor];
[spinner startAnimating];
spinner.center = cell.customView.center;
[cell.customView addSubview:spinner];
return cell;
}
item --;
}
if (item < attachments.count)
{
MXKAttachment *attachment = attachments[item];
NSString *mimeType = attachment.contentInfo[@"mimetype"];
// Use the cached thumbnail (if any) as preview
UIImage* preview = [attachment getCachedThumbnail];
// Check attachment type
if ((attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeSticker) && attachment.contentURL)
{
if ([mimeType isEqualToString:@"image/gif"])
{
cell.mxkImageView.hidden = YES;
// Set the preview as the default image even if the image view is hidden. It will be used during zoom out animation.
cell.mxkImageView.image = preview;
cell.customView.hidden = NO;
// Animated gif is displayed in webview
CGFloat minSize = (cell.frame.size.width < cell.frame.size.height) ? cell.frame.size.width : cell.frame.size.height;
CGFloat width, height;
if (attachment.contentInfo[@"w"] && attachment.contentInfo[@"h"])
{
width = [attachment.contentInfo[@"w"] integerValue];
height = [attachment.contentInfo[@"h"] integerValue];
if (width > minSize || height > minSize)
{
if (width > height)
{
height = (height * minSize) / width;
height = floorf(height / 2) * 2;
width = minSize;
}
else
{
width = (width * minSize) / height;
width = floorf(width / 2) * 2;
height = minSize;
}
}
else
{
width = minSize;
height = minSize;
}
}
else
{
width = minSize;
height = minSize;
}
WKWebView *animatedGifViewer = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
animatedGifViewer.center = cell.customView.center;
animatedGifViewer.opaque = NO;
animatedGifViewer.backgroundColor = cell.customView.backgroundColor;
animatedGifViewer.contentMode = UIViewContentModeScaleAspectFit;
animatedGifViewer.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
animatedGifViewer.userInteractionEnabled = NO;
[cell.customView addSubview:animatedGifViewer];
UIImageView *previewImage = [[UIImageView alloc] initWithFrame:animatedGifViewer.frame];
previewImage.contentMode = animatedGifViewer.contentMode;
previewImage.autoresizingMask = animatedGifViewer.autoresizingMask;
previewImage.image = preview;
previewImage.center = cell.customView.center;
[cell.customView addSubview:previewImage];
MXKPieChartView *pieChartView = [[MXKPieChartView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
pieChartView.progress = 0;
pieChartView.progressColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.25];
pieChartView.unprogressColor = [UIColor clearColor];
pieChartView.autoresizingMask = animatedGifViewer.autoresizingMask;
pieChartView.center = cell.customView.center;
[cell.customView addSubview:pieChartView];
// Add download progress observer
NSString *downloadId = attachment.downloadId;
cell.notificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXMediaLoader *loader = (MXMediaLoader*)notif.object;
if ([loader.downloadId isEqualToString:downloadId])
{
// update the image
switch (loader.state) {
case MXMediaLoaderStateDownloadInProgress:
{
NSNumber* progressNumber = [loader.statisticsDict valueForKey:kMXMediaLoaderProgressValueKey];
if (progressNumber)
{
pieChartView.progress = progressNumber.floatValue;
}
break;
}
default:
break;
}
}
}];
void (^onDownloaded)(NSData *) = ^(NSData *data){
if (cell.notificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:cell.notificationObserver];
cell.notificationObserver = nil;
}
if (animatedGifViewer.superview)
{
[animatedGifViewer loadData:data MIMEType:@"image/gif" characterEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:@"http://"]];
[pieChartView removeFromSuperview];
[previewImage removeFromSuperview];
}
};
void (^onFailure)(NSError *) = ^(NSError *error){
if (cell.notificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:cell.notificationObserver];
cell.notificationObserver = nil;
}
MXLogDebug(@"[MXKAttachmentsVC] gif download failed");
// Notify MatrixKit user
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
};
[attachment getAttachmentData:^(NSData *data) {
onDownloaded(data);
} failure:^(NSError *error) {
onFailure(error);
}];
}
else if (indexPath.item == currentVisibleItemIndex)
{
// Load high res image
cell.mxkImageView.stretchable = YES;
[cell.mxkImageView setAttachment:attachment];
}
else
{
// Use the thumbnail here - Full res images should only be downloaded explicitly when requested (see [self refreshCurrentVisibleItemIndex])
cell.mxkImageView.stretchable = YES;
[cell.mxkImageView setAttachmentThumb:attachment];
}
}
else if (attachment.type == MXKAttachmentTypeVideo && attachment.contentURL)
{
cell.mxkImageView.mediaFolder = attachment.eventRoomId;
cell.mxkImageView.stretchable = NO;
cell.mxkImageView.enableInMemoryCache = YES;
// Display video thumbnail, the video is played only when user selects this cell
[cell.mxkImageView setAttachmentThumb:attachment];
cell.centerIcon.image = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"play"];
cell.centerIcon.hidden = NO;
}
// Add gesture recognizers on collection cell to handle tap and long press on collection cell.
// Note: tap gesture recognizer is required here because mxkImageView enables user interaction to allow image stretching.
// [collectionView:didSelectItemAtIndexPath] is not triggered when mxkImageView is displayed.
UITapGestureRecognizer *cellTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellTap:)];
[cellTapGesture setNumberOfTouchesRequired:1];
[cellTapGesture setNumberOfTapsRequired:1];
cell.tag = item;
[cell addGestureRecognizer:cellTapGesture];
UILongPressGestureRecognizer *cellLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellLongPress:)];
[cell addGestureRecognizer:cellLongPressGesture];
}
return cell;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger item = indexPath.item;
BOOL navigationBarDisplayHandled = NO;
if (isBackPaginationInProgress)
{
if (item == 0)
{
return;
}
item --;
}
// Check whether the selected attachment is a video
if (item < attachments.count)
{
MXKAttachment *attachment = attachments[item];
if (attachment.type == MXKAttachmentTypeVideo && attachment.contentURL)
{
MXKMediaCollectionViewCell *selectedCell = (MXKMediaCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
// Add movie player if none
if (selectedCell.moviePlayer == nil)
{
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
selectedCell.moviePlayer = [[AVPlayerViewController alloc] init];
if (selectedCell.moviePlayer != nil)
{
// Switch in custom view
selectedCell.mxkImageView.hidden = YES;
selectedCell.customView.hidden = NO;
// Report the video preview
UIImageView *previewImage = [[UIImageView alloc] initWithFrame:selectedCell.customView.frame];
previewImage.contentMode = UIViewContentModeScaleAspectFit;
previewImage.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
previewImage.image = selectedCell.mxkImageView.image;
previewImage.center = selectedCell.customView.center;
[selectedCell.customView addSubview:previewImage];
selectedCell.moviePlayer.videoGravity = AVLayerVideoGravityResizeAspect;
selectedCell.moviePlayer.view.frame = selectedCell.customView.frame;
selectedCell.moviePlayer.view.center = selectedCell.customView.center;
selectedCell.moviePlayer.view.hidden = YES;
[selectedCell.customView addSubview:selectedCell.moviePlayer.view];
// Force the video to stay in fullscreen
NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:selectedCell.moviePlayer.view
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:selectedCell.customView
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
NSLayoutConstraint *leadingConstraint = [NSLayoutConstraint constraintWithItem:selectedCell.moviePlayer.view
attribute:NSLayoutAttributeLeading
relatedBy:0
toItem:selectedCell.customView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0];
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:selectedCell.moviePlayer.view
attribute:NSLayoutAttributeBottom
relatedBy:0
toItem:selectedCell.customView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:0];
NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:selectedCell.moviePlayer.view
attribute:NSLayoutAttributeTrailing
relatedBy:0
toItem:selectedCell.customView
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0];
selectedCell.moviePlayer.view.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[topConstraint, leadingConstraint, bottomConstraint, trailingConstraint]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerPlaybackDidFinishWithErrorNotification:)
name:AVPlayerItemFailedToPlayToEndTimeNotification
object:nil];
}
}
if (selectedCell.moviePlayer)
{
if (selectedCell.moviePlayer.player.status == AVPlayerStatusReadyToPlay)
{
// Show or hide the navigation bar
// The video controls bar display is automatically managed by MPMoviePlayerController.
// We have no control on it and no notifications about its displays changes.
// The following code synchronizes the display of the navigation bar with the
// MPMoviePlayerController controls bar.
// Check the MPMoviePlayerController controls bar display status by an hacky way
BOOL controlsVisible = NO;
for(id views in [[selectedCell.moviePlayer view] subviews])
{
for(id subViews in [views subviews])
{
for (id controlView in [subViews subviews])
{
if ([controlView isKindOfClass:[UIView class]] && ((UIView*)controlView).tag == 1004)
{
UIView *subView = (UIView*)controlView;
controlsVisible = (subView.alpha <= 0.0) ? NO : YES;
}
}
}
}
// Apply the same display to the navigation bar
self.navigationBarContainer.hidden = !controlsVisible;
navigationBarDisplayHandled = YES;
if (!self.navigationBarContainer.hidden)
{
// Automaticaly hide the nav bar after 5s. This is the same timer value that
// MPMoviePlayerController uses for its controls bar
[navigationBarDisplayTimer invalidate];
navigationBarDisplayTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(hideNavigationBar) userInfo:self repeats:NO];
}
}
else if (!self.isLoadingVideo)
{
self.isLoadingVideo = YES;
MXKPieChartView *pieChartView = [[MXKPieChartView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
pieChartView.progress = 0;
pieChartView.progressColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.25];
pieChartView.unprogressColor = [UIColor clearColor];
pieChartView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
pieChartView.center = selectedCell.customView.center;
[selectedCell.customView addSubview:pieChartView];
// Add download progress observer
NSString *downloadId = attachment.downloadId;
selectedCell.notificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXMediaLoader *loader = (MXMediaLoader*)notif.object;
if ([loader.downloadId isEqualToString:downloadId])
{
// update progress
switch (loader.state) {
case MXMediaLoaderStateDownloadInProgress:
{
NSNumber* progressNumber = [loader.statisticsDict valueForKey:kMXMediaLoaderProgressValueKey];
if (progressNumber)
{
pieChartView.progress = progressNumber.floatValue;
}
break;
}
default:
break;
}
}
}];
[self prepareVideoForItem:item success:^{
if (selectedCell.notificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:selectedCell.notificationObserver];
selectedCell.notificationObserver = nil;
}
if (selectedCell.moviePlayer.view.superview)
{
selectedCell.moviePlayer.view.hidden = NO;
selectedCell.centerIcon.hidden = YES;
selectedCell.moviePlayer.player = [AVPlayer playerWithURL:[NSURL fileURLWithPath:self->videoFile]];
[selectedCell.moviePlayer.player play];
[pieChartView removeFromSuperview];
self.isLoadingVideo = NO;
[self hideNavigationBar];
}
} failure:^(NSError *error) {
if (selectedCell.notificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:selectedCell.notificationObserver];
selectedCell.notificationObserver = nil;
}
MXLogDebug(@"[MXKAttachmentsVC] video download failed");
[pieChartView removeFromSuperview];
self.isLoadingVideo = NO;
// Display the navigation bar so that the user can leave this screen
self.navigationBarContainer.hidden = NO;
// Notify MatrixKit user
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
}];
// Do not animate the navigation bar on video playback preparing
return;
}
}
}
}
// Animate navigation bar if it is has not been handled
if (!navigationBarDisplayHandled)
{
if (self.navigationBarContainer.hidden)
{
self.navigationBarContainer.hidden = NO;
[navigationBarDisplayTimer invalidate];
navigationBarDisplayTimer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(hideNavigationBar) userInfo:self repeats:NO];
}
else
{
[self hideNavigationBar];
}
}
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
// Here the cell is not displayed anymore, but it may be displayed again if the user swipes on it.
if ([cell isKindOfClass:[MXKMediaCollectionViewCell class]])
{
MXKMediaCollectionViewCell *mediaCollectionViewCell = (MXKMediaCollectionViewCell*)cell;
// Check whether a video was playing in this cell.
if (mediaCollectionViewCell.moviePlayer)
{
// This cell concerns an attached video.
// We stop the player, and restore the default display based on the video thumbnail
[mediaCollectionViewCell.moviePlayer.player pause];
mediaCollectionViewCell.moviePlayer.player = nil;
mediaCollectionViewCell.moviePlayer = nil;
mediaCollectionViewCell.mxkImageView.hidden = NO;
mediaCollectionViewCell.centerIcon.hidden = NO;
mediaCollectionViewCell.customView.hidden = YES;
// Remove potential media download observer
if (mediaCollectionViewCell.notificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:mediaCollectionViewCell.notificationObserver];
mediaCollectionViewCell.notificationObserver = nil;
}
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Detect horizontal bounce at the beginning of the collection to trigger pagination
if (scrollView == self.attachmentsCollection && !isBackPaginationInProgress && !self.complete && self.delegate)
{
if (scrollView.contentOffset.x < -30)
{
isBackPaginationInProgress = YES;
[self.attachmentsCollection insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]]];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (scrollView == self.attachmentsCollection)
{
if (isBackPaginationInProgress)
{
MXKAttachment *attachment = self.attachments.firstObject;
self.complete = ![self.delegate attachmentsViewController:self paginateAttachmentBefore:attachment.eventId];
}
else
{
[self refreshCurrentVisibleCell];
}
}
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
// Use the window from the app delegate as this can be called before the view is presented.
return LegacyAppDelegate.theDelegate.window.bounds.size;
}
#pragma mark - Movie Player
- (void)moviePlayerPlaybackDidFinishWithErrorNotification:(NSNotification *)notification
{
NSDictionary *notificationUserInfo = [notification userInfo];
NSError *mediaPlayerError = [notificationUserInfo objectForKey:AVPlayerItemFailedToPlayToEndTimeErrorKey];
if (mediaPlayerError)
{
MXLogDebug(@"[MXKAttachmentsVC] Playback failed with error description: %@", [mediaPlayerError localizedDescription]);
// Display the navigation bar so that the user can leave this screen
self.navigationBarContainer.hidden = NO;
// Notify MatrixKit user
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:mediaPlayerError];
}
}
#pragma mark - Gesture recognizer
- (void)onCollectionViewCellTap:(UIGestureRecognizer*)gestureRecognizer
{
MXKMediaCollectionViewCell *selectedCell;
UIView *view = gestureRecognizer.view;
if ([view isKindOfClass:[MXKMediaCollectionViewCell class]])
{
selectedCell = (MXKMediaCollectionViewCell*)view;
}
// Notify the collection view delegate a cell has been selected.
if (selectedCell && selectedCell.tag < attachments.count)
{
[self collectionView:self.attachmentsCollection didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:(isBackPaginationInProgress ? selectedCell.tag + 1: selectedCell.tag) inSection:0]];
}
}
- (void)onCollectionViewCellLongPress:(UIGestureRecognizer*)gestureRecognizer
{
MXKMediaCollectionViewCell *selectedCell;
if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
UIView *view = gestureRecognizer.view;
if ([view isKindOfClass:[MXKMediaCollectionViewCell class]])
{
selectedCell = (MXKMediaCollectionViewCell*)view;
}
}
// Notify the collection view delegate a cell has been selected.
if (selectedCell && selectedCell.tag < attachments.count)
{
MXKAttachment *attachment = attachments[selectedCell.tag];
if (currentAlert)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
}
__weak __typeof(self) weakSelf = self;
currentAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
if ([MXKAppSettings standardAppSettings].messageDetailsAllowSaving)
{
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n save]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
typeof(self) self = weakSelf;
self->currentAlert = nil;
[self startActivityIndicator];
[attachment save:^{
typeof(self) self = weakSelf;
[self stopActivityIndicator];
} failure:^(NSError *error) {
typeof(self) self = weakSelf;
[self stopActivityIndicator];
// Notify MatrixKit user
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
}];
}]];
}
if ([MXKAppSettings standardAppSettings].messageDetailsAllowCopyingMedia)
{
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n copyButtonName]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
typeof(self) self = weakSelf;
self->currentAlert = nil;
[self startActivityIndicator];
[attachment copy:^{
typeof(self) self = weakSelf;
[self stopActivityIndicator];
} failure:^(NSError *error) {
typeof(self) self = weakSelf;
[self stopActivityIndicator];
// Notify MatrixKit user
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
}];
}]];
}
if ([MXKAppSettings standardAppSettings].messageDetailsAllowSharing)
{
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n share]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
MXWeakify(self);
self->currentAlert = nil;
[self startActivityIndicator];
[attachment prepareShare:^(NSURL *fileURL) {
MXStrongifyAndReturnIfNil(self);
[self stopActivityIndicator];
self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL];
[self->documentInteractionController setDelegate:self];
self->currentSharedAttachment = attachment;
if (![self->documentInteractionController presentOptionsMenuFromRect:self.view.frame inView:self.view animated:YES])
{
self->documentInteractionController = nil;
[attachment onShareEnded];
self->currentSharedAttachment = nil;
}
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
[self stopActivityIndicator];
// Notify MatrixKit user
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
}];
}]];
}
if ([MXMediaManager existingDownloaderWithIdentifier:attachment.downloadId])
{
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancelDownload]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
typeof(self) self = weakSelf;
self->currentAlert = nil;
// Get again the loader
MXMediaLoader *loader = [MXMediaManager existingDownloaderWithIdentifier:attachment.downloadId];
if (loader)
{
[loader cancel];
}
}]];
}
if (currentAlert.actions.count)
{
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
typeof(self) self = weakSelf;
self->currentAlert = nil;
}]];
[currentAlert popoverPresentationController].sourceView = _attachmentsCollection;
[currentAlert popoverPresentationController].sourceRect = _attachmentsCollection.bounds;
[self presentViewController:currentAlert animated:YES completion:nil];
}
else
{
currentAlert = nil;
}
}
}
#pragma mark - UIDocumentInteractionControllerDelegate
- (UIViewController *)documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller
{
return self;
}
// Preview presented/dismissed on document. Use to set up any HI underneath.
- (void)documentInteractionControllerWillBeginPreview:(UIDocumentInteractionController *)controller
{
documentInteractionController = controller;
}
- (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller
{
documentInteractionController = nil;
if (currentSharedAttachment)
{
[currentSharedAttachment onShareEnded];
currentSharedAttachment = nil;
}
}
- (void)documentInteractionControllerDidDismissOptionsMenu:(UIDocumentInteractionController *)controller
{
documentInteractionController = nil;
if (currentSharedAttachment)
{
[currentSharedAttachment onShareEnded];
currentSharedAttachment = nil;
}
}
- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller
{
documentInteractionController = nil;
if (currentSharedAttachment)
{
[currentSharedAttachment onShareEnded];
currentSharedAttachment = nil;
}
}
#pragma mark - MXKDestinationAttachmentAnimatorDelegate
- (BOOL)prepareSubviewsForTransition:(BOOL)isStartInteraction
{
// Sanity check
if (currentVisibleItemIndex >= attachments.count)
{
return NO;
}
MXKAttachment *attachment = attachments[currentVisibleItemIndex];
NSString *mimeType = attachment.contentInfo[@"mimetype"];
// Check attachment type for GIFs - this is required because of the extra WKWebView
if (attachment.type == MXKAttachmentTypeImage && attachment.contentURL && [mimeType isEqualToString:@"image/gif"])
{
MXKMediaCollectionViewCell *cell = (MXKMediaCollectionViewCell *)[self.attachmentsCollection.visibleCells firstObject];
UIView *customView = cell.customView;
for (UIView *v in customView.subviews)
{
if ([v isKindOfClass:[WKWebView class]])
{
v.hidden = isStartInteraction;
return YES;
}
}
}
return NO;
}
- (UIImageView *)finalImageView
{
MXKMediaCollectionViewCell *cell = (MXKMediaCollectionViewCell *)[self.attachmentsCollection.visibleCells firstObject];
return cell.mxkImageView.imageView;
}
#pragma mark - UIViewControllerTransitioningDelegate
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
if (self.customAnimationsEnabled)
{
return [[MXKAttachmentAnimator alloc] initWithAnimationType:PhotoBrowserZoomInAnimation sourceViewController:self.sourceViewController];
}
return nil;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
[self hideNavigationBar];
if (self.customAnimationsEnabled)
{
return [[MXKAttachmentAnimator alloc] initWithAnimationType:PhotoBrowserZoomOutAnimation sourceViewController:self.sourceViewController];
}
return nil;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
//if there is an interaction, use the custom interaction controller to handle it
if (self.interactionController.interactionInProgress)
{
return self.interactionController;
}
return nil;
}
#pragma mark - UINavigationControllerDelegate
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController {
if (self.customAnimationsEnabled && self.interactionController.interactionInProgress)
{
return self.interactionController;
}
return nil;
}
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
if (self.customAnimationsEnabled)
{
if (operation == UINavigationControllerOperationPush)
{
return [[MXKAttachmentAnimator alloc] initWithAnimationType:PhotoBrowserZoomInAnimation sourceViewController:self.sourceViewController];
}
if (operation == UINavigationControllerOperationPop)
{
return [[MXKAttachmentAnimator alloc] initWithAnimationType:PhotoBrowserZoomOutAnimation sourceViewController:self.sourceViewController];
}
return nil;
}
return nil;
}
@end