/* 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 @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 () { /** 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 *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 *)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 )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 setNumberOfTapsRequired:1]; cell.tag = item; [cell addGestureRecognizer:cellTapGesture]; // BWI: allow changing the image zoom level by double tab if ([BWIBuildSettings.shared allowDoubleTapOnImageAttachmentsForZoom]) { UITapGestureRecognizer *cellDoubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellDoubleTap:)]; [cellDoubleTapGesture setNumberOfTapsRequired:2]; cell.tag = item; [cell addGestureRecognizer:cellDoubleTapGesture]; } 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)onCollectionViewCellDoubleTap:(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) { NSInteger item = isBackPaginationInProgress ? selectedCell.tag + 1: selectedCell.tag; 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 == MXKAttachmentTypeImage && attachment.contentURL) { [selectedCell.mxkImageView toggleZoomLevel]; } } } } - (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 )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { if (self.customAnimationsEnabled) { return [[MXKAttachmentAnimator alloc] initWithAnimationType:PhotoBrowserZoomInAnimation sourceViewController:self.sourceViewController]; } return nil; } - (id )animationControllerForDismissedController:(UIViewController *)dismissed { [self hideNavigationBar]; if (self.customAnimationsEnabled) { return [[MXKAttachmentAnimator alloc] initWithAnimationType:PhotoBrowserZoomOutAnimation sourceViewController:self.sourceViewController]; } return nil; } - (id)interactionControllerForDismissal:(id)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 )navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id ) animationController { if (self.customAnimationsEnabled && self.interactionController.interactionInProgress) { return self.interactionController; } return nil; } - (id )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