Merge branch 'feature/5365_photos_picker' into 'develop'

MESSENGER-5365 photos picker

See merge request bwmessenger/bundesmessenger/bundesmessenger-ios!302
This commit is contained in:
Arnfried Griesert
2024-02-16 06:46:13 +00:00
6 changed files with 251 additions and 20 deletions

View File

@@ -713,6 +713,10 @@ class BWIBuildSettings: NSObject {
// MARK: Backup restore
var showMatrixIDinRoomInfoScreen = false
// MARK: New photos picker API
var useNewPhotosPickerAPI = true
// MARK: Contacts List
var showContactIdentifierInDetailRow = true

View File

@@ -335,7 +335,7 @@ typedef enum : NSUInteger
@param selectedVideo an AVAsset that represents the video to send.
@param isPhotoLibraryAsset tell whether the video has been selected from user's photos library.
*/
- (void)sendSelectedVideoAsset:(AVAsset*)selectedVideo isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset;
- (BOOL)sendSelectedVideoAsset:(AVAsset*)selectedVideo isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset;
/**
Handle multiple media attachments according to the compression mode.

View File

@@ -779,7 +779,8 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes)
[self sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
}
- (void)sendSelectedVideoAsset:(AVAsset*)selectedVideo isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
// bwi: added boolean return value to inform the caller if the asset could be sent to the content repository
- (BOOL)sendSelectedVideoAsset:(AVAsset*)selectedVideo isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
// Check condition before saving this media in user's library
if (_enableAutoSaving && !isPhotoLibraryAsset)
@@ -797,21 +798,32 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes)
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:sendVideoAsset:withThumbnail:)])
{
if (![selectedVideo isKindOfClass:[AVURLAsset class]]) {
MXLogDebug(@"sendSelectedVideoAsset failed because asset is not an AVURLAsset");
return false;
}
// 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];
if(imageRef) {
// Finalize video attachment
UIImage* videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CFRelease(imageRef);
[self.delegate roomInputToolbarView:self sendVideoAsset:selectedVideo withThumbnail:videoThumbnail];
return true;
} else {
MXLogDebug(@"sendSelectedVideoAsset failed because imageRef is nil");
return false;
}
}
else
{
MXLogDebug(@"[RoomInputToolbarView] Attach video is not supported");
return false;
}
}

View File

@@ -17,6 +17,7 @@
import Foundation
import UIKit
import AVFoundation
import PhotosUI
@objc protocol SingleImagePickerPresenterDelegate: AnyObject {
func singleImagePickerPresenter(_ presenter: SingleImagePickerPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
@@ -122,12 +123,26 @@ final class SingleImagePickerPresenter: NSObject {
guard let presentingViewController = self.presentingViewController else {
return
}
let mediaPickerPresenter = MediaPickerCoordinatorBridgePresenter(session: self.session, mediaUTIs: [.image], allowsMultipleSelection: false)
mediaPickerPresenter.delegate = self
mediaPickerPresenter.present(from: presentingViewController, sourceView: sourceView, sourceRect: sourceRect, animated: animated)
self.mediaPickerPresenter = mediaPickerPresenter
if BWIBuildSettings.shared.useNewPhotosPickerAPI {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = PHPickerFilter.any(of: [.images, .videos])
configuration.preferredAssetRepresentationMode = .compatible
configuration.selection = .default
configuration.selectionLimit = 1
configuration.preselectedAssetIdentifiers = []
let pickerViewController = PHPickerViewController(configuration: configuration)
pickerViewController.delegate = self
presentingViewController.present(pickerViewController, animated: true)
} else {
let mediaPickerPresenter = MediaPickerCoordinatorBridgePresenter(session: self.session, mediaUTIs: [.image], allowsMultipleSelection: false)
mediaPickerPresenter.delegate = self
mediaPickerPresenter.present(from: presentingViewController, sourceView: sourceView, sourceRect: sourceRect, animated: animated)
self.mediaPickerPresenter = mediaPickerPresenter
}
}
private func removeImage() {
@@ -170,3 +185,30 @@ extension SingleImagePickerPresenter: MediaPickerCoordinatorBridgePresenterDeleg
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
}
// MARK: - PHPickerViewControllerDelegate (BWI)
extension SingleImagePickerPresenter: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider else {
self.delegate?.singleImagePickerPresenterDidCancel(self)
return
}
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
if let image = image as? UIImage, let imageData = image.pngData() {
let uti = MXKUTI(mimeType: "image/png")
self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: uti)
} else {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
}
}
} else {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
}
}

View File

@@ -3560,13 +3560,35 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendVideoAsset:(AVAsset*)videoAsset withThumbnail:(UIImage*)videoThumbnail
{
// Let the datasource send it and manage the local echo
[roomDataSource sendVideoAsset:videoAsset withThumbnail:videoThumbnail success:nil failure:^(NSError *error)
{
[roomDataSource sendVideoAsset:videoAsset withThumbnail:videoThumbnail success:^(NSString *eventId) {
// bwi: 5365
NSURL *url = [(AVURLAsset *)videoAsset URL];
if(url) {
[self deleteFileAtURL: url];
}
} failure:^(NSError *error) {
// Nothing to do. The video is marked as unsent in the room history by the datasource
MXLogDebug(@"[MXKRoomViewController] sendVideo failed.");
// bwi: 5365
NSURL *url = [(AVURLAsset *)videoAsset URL];
if(url) {
[self deleteFileAtURL: url];
}
}];
}
- (void)deleteFileAtURL:(NSURL *)url {
if (url == nil) {
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if (![fileManager removeItemAtURL:url error:&error]) {
MXLogDebug(@"Cannot delete file at %@", [url absoluteString])
MXLogDebug(@"%@", [error localizedDescription])
}
}
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendFile:(NSURL *)fileLocalURL withMimeType:(NSString*)mimetype
{
// Let the datasource send it and manage the local echo

View File

@@ -89,6 +89,9 @@
#import "GeneratedInterface-Swift.h"
#import <PhotosUI/PhotosUI.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification";
NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification";
const NSTimeInterval kResizeComposerAnimationDuration = .05;
@@ -100,7 +103,7 @@ static CGSize kThreadListBarButtonItemImageSize;
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate, PHPickerViewControllerDelegate>
{
// The preview header
@@ -2334,9 +2337,24 @@ static CGSize kThreadListBarButtonItemImageSize;
sourceView = self.inputToolbarView;
}
[mediaPickerPresenter presentFrom:self sourceView:sourceView sourceRect:sourceView.bounds animated:YES];
self.mediaPickerPresenter = mediaPickerPresenter;
if(BWIBuildSettings.shared.useNewPhotosPickerAPI) {
PHPhotoLibrary *sharedPhotoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] initWithPhotoLibrary:sharedPhotoLibrary];
configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[PHPickerFilter.imagesFilter, PHPickerFilter.videosFilter]];
configuration.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCompatible;
configuration.selection = PHPickerConfigurationSelectionDefault;
configuration.selectionLimit = 1;
configuration.preselectedAssetIdentifiers = @[];
PHPickerViewController *pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:configuration];
pickerViewController.delegate = self;
[self presentViewController:pickerViewController animated:YES completion:nil];
} else {
[mediaPickerPresenter presentFrom:self sourceView:sourceView sourceRect:sourceView.bounds animated:YES];
self.mediaPickerPresenter = mediaPickerPresenter;
}
}
- (void)showRoomCreationModal
@@ -2675,6 +2693,32 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
// bwi: 5365
- (void)sendVideoAssetWithoutCompression:(AVAsset *)videoAsset isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
// Create before sending the message in case of a discussion (direct chat)
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
if(![self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset]) {
NSURL *url = [(AVURLAsset *)videoAsset URL];
if(url) {
[self deleteFileAtURL:url];
}
}
} else {
if([videoAsset isKindOfClass:[AVURLAsset class]]) {
NSURL *url = [(AVURLAsset *)videoAsset URL];
[self deleteFileAtURL:url];
}
}
NSURL *url = [(AVURLAsset *)videoAsset URL];
if(url) {
[self deleteFileAtURL:url];
}
}];
}
- (void)showRoomWithId:(NSString*)roomId
{
if (self.delegate)
@@ -8031,6 +8075,113 @@ static CGSize kThreadListBarButtonItemImageSize;
}];
}
#pragma mark - PHPickerViewControllerDelegate
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results
{
[picker dismissViewControllerAnimated:YES completion:nil];
if (results.firstObject) {
NSItemProvider *itemProvider = results.firstObject.itemProvider;
if ([itemProvider canLoadObjectOfClass:[UIImage class]]) {
[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^(id _Nullable object, NSError * _Nullable error) {
if (error) {
MXLogDebug(@"Uploading photo from photo library failed: %@", error);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
if ([object isKindOfClass:[UIImage class]]) {
UIImage *image = object;
[self uploadImage:image];
}
});
}
}];
} else if ([itemProvider hasItemConformingToTypeIdentifier:@"public.movie"]) {
[itemProvider loadFileRepresentationForTypeIdentifier:@"public.movie" completionHandler:^(NSURL *videoURL, NSError *error) {
if (error) {
MXLogDebug(@"Uploading video from photo library failed: %@", error);
} else {
// First make a copy of the video in the temp directory because the video at videoURL
// is a copy by the iOS photo picker and will be dismissed when this completion block ends
NSURL *videoCopy = [self makeVideoCopy:videoURL];
dispatch_async(dispatch_get_main_queue(), ^{
[self uploadVideo:videoCopy];
});
}
}];
}
}
}
- (void)uploadImage:(UIImage *)image {
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
NSData *imageData = [image dataForPNGRepresentation];
if(imageData) {
[self.inputToolbarView sendSelectedImage:imageData
withMimeType:@"image/png"
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
isPhotoLibraryAsset:YES];
} else {
MXLogDebug(@"Uploading photo from photo library failed because imageData is nil")
}
}
}];
}
- (void)uploadVideo:(NSURL *)url {
if(url) {
NSLog(@"uploadVideo: %@", [url absoluteString]);
AVURLAsset *asset = [AVURLAsset assetWithURL:url];
if(asset) {
[self sendVideoAssetWithoutCompression:asset isPhotoLibraryAsset:YES];
} else {
MXLogDebug(@"Uploading video from photo library failed because asset is nil")
}
}
}
- (NSURL *)makeVideoCopy:(NSURL *)sourceURL {
if (sourceURL == nil) {
return nil;
}
NSString *sourceFilename = [sourceURL lastPathComponent];
NSUUID *uuid = [NSUUID UUID];
NSString *uuidString = [uuid UUIDString];
NSString *destinationFilename = [NSString stringWithFormat:@"%@-%@", uuidString, sourceFilename];
NSURL *tempDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
NSURL *destinationURL = [tempDirURL URLByAppendingPathComponent:destinationFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if ([fileManager copyItemAtURL:sourceURL toURL:destinationURL error:&error]) {
MXLogDebug(@"Successfully copied video to a temp destination %@", [destinationURL absoluteString]);
return destinationURL;
} else {
MXLogDebug(@"Failed to make a temp copy of the video file: %@", error);
return nil;
}
}
- (void)deleteFileAtURL:(NSURL *)url {
if (url == nil) {
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if (![fileManager removeItemAtURL:url error:&error]) {
MXLogDebug(@"Cannot delete file at %@", [url absoluteString])
MXLogDebug(@"%@", [error localizedDescription])
}
}
#pragma mark - RoomCreationModalCoordinatorBridgePresenter
- (void)roomCreationModalCoordinatorBridgePresenterDelegateDidComplete:(RoomCreationModalCoordinatorBridgePresenter *)coordinatorBridgePresenter