diff --git a/Riot/Assets/SharedImages.xcassets/Contents.json b/Riot/Assets/SharedImages.xcassets/Contents.json index da4a164c9..73c00596a 100644 --- a/Riot/Assets/SharedImages.xcassets/Contents.json +++ b/Riot/Assets/SharedImages.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json new file mode 100644 index 000000000..35812152a --- /dev/null +++ b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "radio-button-default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio-button-default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio-button-default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png new file mode 100644 index 000000000..84e419079 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png new file mode 100644 index 000000000..7e6083bc3 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png new file mode 100644 index 000000000..316a8eab7 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json new file mode 100644 index 000000000..a69d70fe6 --- /dev/null +++ b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "radio-button-selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio-button-selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio-button-selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png new file mode 100644 index 000000000..6a744d6be Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png new file mode 100644 index 000000000..67c3bbd64 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png new file mode 100644 index 000000000..a4cd21452 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index bad6db43d..90551f97a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -360,6 +360,7 @@ Tap the + to start adding people."; "room_event_action_redact" = "Remove"; "room_event_action_more" = "More"; "room_event_action_share" = "Share"; +"room_event_action_forward" = "Forward"; "room_event_action_permalink" = "Permalink"; "room_event_action_view_source" = "View Source"; "room_event_action_view_decrypted_source" = "View Decrypted Source"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 83b830e6c..cd4a5b92e 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -211,6 +211,8 @@ internal enum Asset { internal static let cancel = ImageAsset(name: "cancel") internal static let e2eVerified = ImageAsset(name: "e2e_verified") internal static let horizontalLogo = ImageAsset(name: "horizontal_logo") + internal static let radioButtonDefault = ImageAsset(name: "radio-button-default") + internal static let radioButtonSelected = ImageAsset(name: "radio-button-selected") } } // swiftlint:enable identifier_name line_length nesting type_body_length type_name diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index db36f708a..2801397ab 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2887,6 +2887,10 @@ public class VectorL10n: NSObject { public static var roomEventActionEdit: String { return VectorL10n.tr("Vector", "room_event_action_edit") } + /// Forward + public static var roomEventActionForward: String { + return VectorL10n.tr("Vector", "room_event_action_forward") + } /// Reason for kicking this user public static var roomEventActionKickPromptReason: String { return VectorL10n.tr("Vector", "room_event_action_kick_prompt_reason") diff --git a/Riot/Managers/AppInfo/BuildInfo.m b/Riot/Managers/AppInfo/BuildInfo.m index 0853efe94..a1dbffe14 100644 --- a/Riot/Managers/AppInfo/BuildInfo.m +++ b/Riot/Managers/AppInfo/BuildInfo.m @@ -16,7 +16,11 @@ #import "BuildInfo.h" +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else #import "Riot-Swift.h" +#endif #define MAKE_STRING(x) #x #define MAKE_NS_STRING(x) @MAKE_STRING(x) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d32da8258..d5defa7e4 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -16,6 +16,8 @@ limitations under the License. */ +@import MobileCoreServices; + #import "RoomViewController.h" #import "RoomDataSource.h" @@ -106,6 +108,7 @@ #import "AvatarGenerator.h" #import "Tools.h" #import "WidgetManager.h" +#import "ShareManager.h" #import "GBDeviceInfo_iOS.h" @@ -249,6 +252,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; +@property (nonatomic, strong) ShareManager *shareManager; + @property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; @@ -2394,10 +2399,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Set a default title view class without handling tap gesture (Let [self refreshRoomTitle] refresh this view correctly). [self setRoomTitleViewClass:RoomTitleView.class]; - - // Remove details icon - RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView; - + // Remove the shadow image used to hide the bottom border of the navigation bar when the preview header is displayed [mainNavigationController.navigationBar setShadowImage:nil]; [mainNavigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; @@ -3190,6 +3192,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + self.shareManager = [[ShareManager alloc] initWithShareItemProvider:[[SimpleShareItemProvider alloc] initWithTextMessage:selectedComponent.textMessage] + type:ShareManagerTypeForward]; + + MXWeakify(self); + [self.shareManager setCompletionCallback:^(ShareManagerResult result) { + MXStrongifyAndReturnIfNil(self); + [attachment onShareEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; + self.shareManager = nil; + }]; + + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; + }]]; + if (!isJitsiCallEvent) { [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] @@ -3243,6 +3262,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else // Add action for attachment { + if (attachment.type == MXKAttachmentTypeFile || + attachment.type == MXKAttachmentTypeImage || + attachment.type == MXKAttachmentTypeVideo || + attachment.type == MXKAttachmentTypeVoiceMessage) { + + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + self.shareManager = [[ShareManager alloc] initWithShareItemProvider:[[SimpleShareItemProvider alloc] initWithAttachment:attachment] + type:ShareManagerTypeForward]; + + MXWeakify(self); + [self.shareManager setCompletionCallback:^(ShareManagerResult result) { + MXStrongifyAndReturnIfNil(self); + [attachment onShareEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; + self.shareManager = nil; + }]; + + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; + }]]; + } + if (BuildSettings.messageDetailsAllowSave) { if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo) @@ -3340,7 +3382,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; + [self startActivityIndicator]; + [attachment prepareShare:^(NSURL *fileURL) { + [self stopActivityIndicator]; __strong __typeof(weakSelf)self = weakSelf; self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL]; @@ -3355,10 +3400,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } failure:^(NSError *error) { - - //Alert user [self showError:error]; - + [self stopActivityIndicator]; }]; // Start animation in case of download during attachment preparing diff --git a/Riot/target.yml b/Riot/target.yml index 9428387f6..0e9f722da 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -66,6 +66,7 @@ targets: excludes: - "Modules/Room/EmojiPicker/Data/EmojiMart/EmojiJSONStore.swift" - "**/*.strings" # Exclude all strings files + - path: ../RiotShareExtension/Shared # Add separately localizable files # Once a language has enough translations (>80%), it must be declared here diff --git a/RiotShareExtension/Managers/ShareExtensionManager.h b/RiotShareExtension/Managers/ShareExtensionManager.h deleted file mode 100644 index 042d55fb0..000000000 --- a/RiotShareExtension/Managers/ShareExtensionManager.h +++ /dev/null @@ -1,130 +0,0 @@ -/* - Copyright 2017 Aram Sargsyan - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import -#import - -@class ShareExtensionManager; -@class SharePresentingViewController; -@protocol Configurable; - -/** - Posted when the matrix user account and his data has been checked and updated. - The notification object is the MXKAccount instance. - */ -extern NSString *const kShareExtensionManagerDidUpdateAccountDataNotification; - - -/** - The protocol for the manager's delegate - */ -@protocol ShareExtensionManagerDelegate - -@required - -/** - Called when an image is going to be shared to show a compression prompt - @param extensionManager the ShareExtensionManager object that called the method - @param compressionPrompt the prompt that was prepared for the image which is going to be shared - */ -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager showImageCompressionPrompt:(UIAlertController *)compressionPrompt; - -@optional - -/** - Called when the manager starts sending the content to a room - @param extensionManager the ShareExtensionManager object that called the method - @param room the room where content will be sent - */ -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager didStartSendingContentToRoom:(MXRoom *)room; - -/** - Called when the progress of the uploading media changes - @param extensionManager the ShareExtensionManager object that called the method - @param progress the current progress - */ -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager mediaUploadProgress:(CGFloat)progress; - -@end - - -/** - A class used to share content from the extension - */ - -@interface ShareExtensionManager : NSObject - -/** - The share extension context that represents a user's sharing request, also stores the content to be shared - */ -@property (nonatomic) NSExtensionContext *shareExtensionContext; - -/** - The share app extension’s primary view controller. - */ -@property (nonatomic) SharePresentingViewController *primaryViewController; - -/** - The current user account - */ -@property (nonatomic, readonly) MXKAccount *userAccount; - -/** - The shared file store - */ -@property (nonatomic, readonly) MXFileStore *fileStore; - -/** - A delegate used to notify about needed UI changes when sharing - */ -@property (nonatomic, weak) id delegate; - -// Build Settings -@property (nonatomic, readonly) id configuration; - -/** - The singleton instance - */ -+ (instancetype)sharedManager; - -/** - Send the content that the user has chosen to a room - @param room the room to send the content to - @param failureBlock the code to be executed when sharing has failed for whatever reason - note: there is no "successBlock" parameter because when the sharing succeeds, the extension needs to close itself - */ -- (void)sendContentToRoom:(MXRoom *)room failureBlock:(void(^)(NSError *error))failureBlock; - -/** - Checks if there is an image in the user chosen content - @return YES if there is, NO otherwise - */ -- (BOOL)hasImageTypeContent; - -/** - Terminate the extension and return to the app that started it - @param canceled YES if the user chose to cancel the sharing, NO otherwise - */ -- (void)terminateExtensionCanceled:(BOOL)canceled; - -@end - - -@interface NSItemProvider (ShareExtensionManager) - -@property BOOL isLoaded; - -@end diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m deleted file mode 100644 index ea58a7d34..000000000 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ /dev/null @@ -1,1231 +0,0 @@ -/* - Copyright 2017 Aram Sargsyan - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "ShareExtensionManager.h" -#import "SharePresentingViewController.h" -#import -@import MobileCoreServices; -#import "objc/runtime.h" -#include -#import -#import "RiotShareExtension-Swift.h" - -NSString *const kShareExtensionManagerDidUpdateAccountDataNotification = @"kShareExtensionManagerDidUpdateAccountDataNotification"; - -static const CGFloat kLargeImageSizeMaxDimension = 2048.0; - -typedef NS_ENUM(NSInteger, ImageCompressionMode) -{ - ImageCompressionModeNone, - ImageCompressionModeSmall, - ImageCompressionModeMedium, - ImageCompressionModeLarge -}; - - -@interface ShareExtensionManager () - -@property (nonatomic, readwrite) MXKAccount *userAccount; - -@property (nonatomic) NSMutableArray *pendingImages; -@property (nonatomic) NSMutableDictionary *imageUploadProgresses; -@property (nonatomic) ImageCompressionMode imageCompressionMode; -@property (nonatomic) CGFloat actualLargeSize; - -@end - - -@implementation ShareExtensionManager - -#pragma mark - Lifecycle - -+ (instancetype)sharedManager -{ - static ShareExtensionManager *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - sharedInstance = [[self alloc] init]; - - sharedInstance.pendingImages = [NSMutableArray array]; - sharedInstance.imageUploadProgresses = [NSMutableDictionary dictionary]; - - [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(onMediaLoaderStateDidChange:) name:kMXMediaLoaderStateDidChangeNotification object:nil]; - - // Add observer to handle logout - [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(checkUserAccount) name:kMXKAccountManagerDidRemoveAccountNotification object:nil]; - - // Add observer on the Extension host - [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(checkUserAccount) name:NSExtensionHostWillEnterForegroundNotification object:nil]; - - // Add observer to handle memory warning - [NSNotificationCenter.defaultCenter addObserver:sharedInstance selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - - // Set static application settings - sharedInstance->_configuration = [CommonConfiguration new]; - [sharedInstance.configuration setupSettings]; - - // NSLog -> console.log file when not debugging the app - MXLogConfiguration *configuration = [[MXLogConfiguration alloc] init]; - configuration.logLevel = MXLogLevelVerbose; - configuration.logFilesSizeLimit = 0; - configuration.maxLogFilesCount = 10; - configuration.subLogName = @"share"; - - // Redirect NSLogs to files only if we are not debugging - if (!isatty(STDERR_FILENO)) { - configuration.redirectLogsToFiles = YES; - } - - [MXLog configure:configuration]; - }); - return sharedInstance; -} - -- (void)checkUserAccount -{ - // Force account manager to reload account from the local storage. - [[MXKAccountManager sharedManager] forceReloadAccounts]; - - if (self.userAccount) - { - // Check whether the used account is still the first active one - MXKAccount *firstAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; - - // Compare the access token - if (!firstAccount || ![self.userAccount.mxCredentials.accessToken isEqualToString:firstAccount.mxCredentials.accessToken]) - { - // Remove this account - self.userAccount = nil; - } - } - - if (!self.userAccount) - { - // We consider the first enabled account. - // TODO: Handle multiple accounts - self.userAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; - } - - // Reset the file store to reload the room data. - if (_fileStore) - { - [_fileStore close]; - _fileStore = nil; - } - - if (self.userAccount) - { - _fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials]; - } - - // Post notification - [[NSNotificationCenter defaultCenter] postNotificationName:kShareExtensionManagerDidUpdateAccountDataNotification object:self.userAccount userInfo:nil]; -} - -#pragma mark - Public - -- (void)setShareExtensionContext:(NSExtensionContext *)shareExtensionContext -{ - _shareExtensionContext = shareExtensionContext; - - // Set up runtime language on each context update. - NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; - NSString *language = [sharedUserDefaults objectForKey:@"appLanguage"]; - [NSBundle mxk_setLanguage:language]; - [NSBundle mxk_setFallbackLanguage:@"en"]; - - // Check the current matrix user. - [self checkUserAccount]; -} - -- (void)sendContentToRoom:(MXRoom *)room failureBlock:(void(^)(NSError *error))failureBlock -{ - [self resetPendingData]; - - NSString *UTTypeText = (__bridge NSString *)kUTTypeText; - NSString *UTTypeURL = (__bridge NSString *)kUTTypeURL; - NSString *UTTypeImage = (__bridge NSString *)kUTTypeImage; - NSString *UTTypeVideo = (__bridge NSString *)kUTTypeVideo; - NSString *UTTypeFileUrl = (__bridge NSString *)kUTTypeFileURL; - NSString *UTTypeMovie = (__bridge NSString *)kUTTypeMovie; - - BOOL areAllAttachmentsImages = [self areAllAttachmentsImages]; - NSMutableArray *pendingImagesItemProviders = [NSMutableArray new]; // Used to keep NSItemProvider associated to pending images (used only when all items are images). - - __block NSError *firstRequestError = nil; - __block NSMutableArray *returningExtensionItems = [NSMutableArray new]; - dispatch_group_t requestsGroup = dispatch_group_create(); - - void (^requestSuccess)(NSExtensionItem*) = ^(NSExtensionItem *extensionItem) { - if (extensionItem && ![returningExtensionItems containsObject:extensionItem]) - { - [returningExtensionItems addObject:extensionItem]; - } - - dispatch_group_leave(requestsGroup); - }; - - void (^requestFailure)(NSError*) = ^(NSError *requestError) { - if (requestError && !firstRequestError) - { - firstRequestError = requestError; - } - - dispatch_group_leave(requestsGroup); - }; - - __weak typeof(self) weakSelf = self; - - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) - { - for (NSItemProvider *itemProvider in item.attachments) - { - if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeFileUrl]) - { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeFileUrl options:nil completionHandler:^(NSURL *fileUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendFileWithUrl:fileUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); - - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeText]) - { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeText options:nil completionHandler:^(NSString *text, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendText:text - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); - - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeURL]) - { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeURL options:nil completionHandler:^(NSURL *url, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendText:url.absoluteString - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); - - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeImage]) - { - dispatch_group_enter(requestsGroup); - - itemProvider.isLoaded = NO; - - [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id _Nullable itemProviderItem, NSError * _Null_unspecified error) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - itemProvider.isLoaded = YES; - - NSData *imageData; - - if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) - { - imageData = (NSData*)itemProviderItem; - } - else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) - { - NSURL *imageURL = (NSURL*)itemProviderItem; - imageData = [NSData dataWithContentsOfURL:imageURL]; - } - else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) - { - // An application can share directly an UIImage. - // The most common case is screenshot sharing without saving to file. - // As screenshot using PNG format when they are saved to file we also use PNG format when saving UIImage to NSData. - UIImage *image = (UIImage*)itemProviderItem; - imageData = UIImagePNGRepresentation(image); - } - - if (imageData) - { - if (areAllAttachmentsImages) - { - [self.pendingImages addObject:imageData]; - [pendingImagesItemProviders addObject:itemProvider]; - } - else - { - CGSize imageSize = [self imageSizeFromImageData:imageData]; - self.imageCompressionMode = ImageCompressionModeNone; - self.actualLargeSize = MAX(imageSize.width, imageSize.height); - - [self sendImageData:imageData - withProvider:itemProvider - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - } - else - { - MXLogDebug(@"[ShareExtensionManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); - dispatch_group_leave(requestsGroup); - } - - // Only prompt for image resize if all items are images - // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. - if (areAllAttachmentsImages) - { - if ([self areAttachmentsFullyLoaded]) - { - UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ - [self sendImageDatas:self.pendingImages - withProviders:pendingImagesItemProviders - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - }]; - - if (compressionPrompt) - { - [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; - } - } - else - { - dispatch_group_leave(requestsGroup); - } - } - } - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeVideo]) - { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); - - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeMovie]) - { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); - - }]; - } - } - } - - dispatch_group_notify(requestsGroup, dispatch_get_main_queue(), ^{ - [self resetPendingData]; - - if (firstRequestError) - { - if (failureBlock) - { - failureBlock(firstRequestError); - } - } - else - { - [self completeRequestReturningItems:returningExtensionItems completionHandler:nil]; - } - }); -} - -- (BOOL)hasImageTypeContent -{ - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) - { - for (NSItemProvider *itemProvider in item.attachments) - { - if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeImage]) - { - return YES; - } - } - } - return NO; -} - -- (void)terminateExtensionCanceled:(BOOL)canceled -{ - if (canceled) - { - [self.shareExtensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; - } - else - { - [self.shareExtensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; - } - - [self.primaryViewController destroy]; - self.primaryViewController = nil; - - // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. - // For now, we force the share extension to exit and free memory. - [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; -} - -#pragma mark - Private - -- (void)resetPendingData -{ - [self.pendingImages removeAllObjects]; - [self.imageUploadProgresses removeAllObjects]; -} - -- (void)completeRequestReturningItems:(nullable NSArray *)items completionHandler:(void(^ __nullable)(BOOL expired))completionHandler; -{ - [self.shareExtensionContext completeRequestReturningItems:items completionHandler:completionHandler]; - - [self.primaryViewController destroy]; - self.primaryViewController = nil; - - // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. - // For now, we force the share extension to exit and free memory. - [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; -} - -- (BOOL)isAPendingImageNotOrientedUp -{ - BOOL isAPendingImageNotOrientedUp = NO; - - for (NSData *imageData in self.pendingImages) - { - if ([self isImageOrientationNotUpOrUndeterminedForImageData:imageData]) - { - isAPendingImageNotOrientedUp = YES; - break; - } - } - - return isAPendingImageNotOrientedUp; -} - -// TODO: When select multiple images: -// - Enhance prompt to display sum of all file sizes for each compression. -// - Find a way to choose compression sizes for all images. -- (UIAlertController *)compressionPromptForPendingImagesWithShareBlock:(void(^)(void))shareBlock -{ - if (!self.pendingImages.count) - { - return nil; - } - - UIAlertController *compressionPrompt; - BOOL isAPendingImageNotOrientedUp = [self isAPendingImageNotOrientedUp]; - - NSData *firstImageData = self.pendingImages.firstObject; - UIImage *firstImage = [UIImage imageWithData:firstImageData]; - - // Get available sizes for this image - MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:firstImage originalFileSize:firstImageData.length]; - - // Apply the compression mode - if (compressionSizes.small.fileSize || compressionSizes.medium.fileSize || compressionSizes.large.fileSize) - { - __weak typeof(self) weakSelf = self; - - compressionPrompt = [UIAlertController alertControllerWithTitle:[MatrixKitL10n attachmentSizePromptTitle] - message:[MatrixKitL10n attachmentSizePromptMessage] - preferredStyle:UIAlertControllerStyleActionSheet]; - - if (compressionSizes.small.fileSize) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.small.fileSize]; - - NSString *title = [MatrixKitL10n attachmentSmall:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Send the small image - self.imageCompressionMode = ImageCompressionModeSmall; - - [self logCompressionSizeChoice:compressionSizes.large]; - - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - if (compressionSizes.medium.fileSize) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.medium.fileSize]; - - NSString *title = [MatrixKitL10n attachmentMedium:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Send the medium image - self.imageCompressionMode = ImageCompressionModeMedium; - - [self logCompressionSizeChoice:compressionSizes.large]; - - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - // Do not offer the possibility to resize an image with a dimension above kLargeImageSizeMaxDimension, to prevent the risk of memory limit exception. - // TODO: Remove this condition when issue https://github.com/vector-im/riot-ios/issues/2341 will be fixed. - if (compressionSizes.large.fileSize && (MAX(compressionSizes.large.imageSize.width, compressionSizes.large.imageSize.height) <= kLargeImageSizeMaxDimension)) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.large.fileSize]; - - NSString *title = [MatrixKitL10n attachmentLarge:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Send the large image - self.imageCompressionMode = ImageCompressionModeLarge; - self.actualLargeSize = compressionSizes.actualLargeSize; - - [self logCompressionSizeChoice:compressionSizes.large]; - - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate - if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize]; - - NSString *title = [MatrixKitL10n attachmentOriginal:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self.imageCompressionMode = ImageCompressionModeNone; - - [self logCompressionSizeChoice:compressionSizes.large]; - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] - style:UIAlertActionStyleCancel - handler:nil]]; - - - } - else - { - if (isAPendingImageNotOrientedUp && self.pendingImages.count > 1) - { - self.imageCompressionMode = ImageCompressionModeSmall; - } - else - { - self.imageCompressionMode = ImageCompressionModeNone; - } - - MXLogDebug(@"[ShareExtensionManager] Send %lu image(s) without compression prompt using compression mode: %ld", (unsigned long)self.pendingImages.count, (long)self.imageCompressionMode); - - if (shareBlock) - { - shareBlock(); - } - } - - return compressionPrompt; -} - -- (void)didStartSendingToRoom:(MXRoom *)room -{ - if ([self.delegate respondsToSelector:@selector(shareExtensionManager:didStartSendingContentToRoom:)]) - { - [self.delegate shareExtensionManager:self didStartSendingContentToRoom:room]; - } -} - -- (BOOL)areAttachmentsFullyLoaded -{ - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) - { - for (NSItemProvider *itemProvider in item.attachments) - { - if (itemProvider.isLoaded == NO) - { - return NO; - } - } - } - return YES; -} - -- (BOOL)areAllAttachmentsImages -{ - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) - { - for (NSItemProvider *itemProvider in item.attachments) - { - if (![itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeImage]) - { - return NO; - } - } - } - return YES; -} - -- (NSString*)utiFromImageTypeItemProvider:(NSItemProvider*)itemProvider -{ - NSString *uti; - - NSString *utiPNG = (__bridge NSString *)kUTTypePNG; - NSString *utiJPEG = (__bridge NSString *)kUTTypeJPEG; - - if ([itemProvider hasItemConformingToTypeIdentifier:utiPNG]) - { - uti = utiPNG; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:utiJPEG]) - { - uti = utiJPEG; - } - else - { - uti = itemProvider.registeredTypeIdentifiers.firstObject; - } - - return uti; -} - -- (NSString*)utiFromImageData:(NSData*)imageData -{ - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); - NSString *uti = (NSString*)CGImageSourceGetType(imageSource); - CFRelease(imageSource); - return uti; -} - -- (NSString*)mimeTypeFromUTI:(NSString*)uti -{ - return (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)uti, kUTTagClassMIMEType); -} - -- (BOOL)isResizingSupportedForImageData:(NSData*)imageData -{ - NSString *imageUTI = [self utiFromImageData:imageData]; - return [self isResizingSupportedForUTI:imageUTI]; -} - -- (BOOL)isResizingSupportedForUTI:(NSString*)imageUTI -{ - if ([imageUTI isEqualToString:(__bridge NSString *)kUTTypePNG] || [imageUTI isEqualToString:(__bridge NSString *)kUTTypeJPEG]) - { - return YES; - } - return NO; -} - -- (CGSize)imageSizeFromImageData:(NSData*)imageData -{ - CGFloat width = 0.0f; - CGFloat height = 0.0f; - - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); - - CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); - - CFRelease(imageSource); - - if (imageProperties != NULL) - { - CFNumberRef widthNumber = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth); - CFNumberRef heightNumber = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight); - CFNumberRef orientationNumber = CFDictionaryGetValue(imageProperties, kCGImagePropertyOrientation); - - if (widthNumber != NULL) - { - CFNumberGetValue(widthNumber, kCFNumberCGFloatType, &width); - } - - if (heightNumber != NULL) - { - CFNumberGetValue(heightNumber, kCFNumberCGFloatType, &height); - } - - // Check orientation and flip size if required - if (orientationNumber != NULL) - { - int orientation; - CFNumberGetValue(orientationNumber, kCFNumberIntType, &orientation); - - // For orientation from kCGImagePropertyOrientationLeftMirrored to kCGImagePropertyOrientationLeft flip size - if (orientation >= 5) - { - CGFloat tempWidth = width; - width = height; - height = tempWidth; - } - } - - CFRelease(imageProperties); - } - - return CGSizeMake(width, height); -} - -- (NSNumber*)cgImageimageOrientationNumberFromImageData:(NSData*)imageData -{ - NSNumber *orientationNumber; - - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); - - CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); - - CFRelease(imageSource); - - if (imageProperties != NULL) - { - CFNumberRef orientationNum = CFDictionaryGetValue(imageProperties, kCGImagePropertyOrientation); - - // Check orientation and flip size if required - if (orientationNum != NULL) - { - orientationNumber = (__bridge NSNumber *)orientationNum; - } - - CFRelease(imageProperties); - } - - return orientationNumber; -} - -- (BOOL)isImageOrientationNotUpOrUndeterminedForImageData:(NSData*)imageData -{ - BOOL isImageNotOrientedUp = YES; - - NSNumber *cgImageOrientationNumber = [self cgImageimageOrientationNumberFromImageData:imageData]; - - if (cgImageOrientationNumber && cgImageOrientationNumber.unsignedIntegerValue == (NSUInteger)kCGImagePropertyOrientationUp) - { - isImageNotOrientedUp = NO; - } - - return isImageNotOrientedUp; -} - -- (void)logCompressionSizeChoice:(MXKImageCompressionSize)compressionSize -{ - NSString *fileSize = [MXTools fileSizeToString:compressionSize.fileSize round:NO]; - NSUInteger imageWidth = compressionSize.imageSize.width; - NSUInteger imageHeight = compressionSize.imageSize.height; - - MXLogDebug(@"[ShareExtensionManager] User choose image compression with output size %lu x %lu (output file size: %@)", (unsigned long)imageWidth, (unsigned long)imageHeight, fileSize); - MXLogDebug(@"[ShareExtensionManager] Number of images to send: %lu", (unsigned long)self.pendingImages.count); -} - -// Log memory usage. -// NOTE: This result may not be reliable for all iOS versions (see https://forums.developer.apple.com/thread/64665 for more information). -- (void)logMemoryUsage -{ - struct task_basic_info basicInfo; - mach_msg_type_number_t size = TASK_BASIC_INFO_COUNT; - kern_return_t kerr = task_info(mach_task_self(), - TASK_BASIC_INFO, - (task_info_t)&basicInfo, - &size); - - vm_size_t memoryUsedInBytes = basicInfo.resident_size; - CGFloat memoryUsedInMegabytes = memoryUsedInBytes / (1024*1024); - - if (kerr == KERN_SUCCESS) - { - MXLogDebug(@"[ShareExtensionManager] Memory in use (in MB): %f", memoryUsedInMegabytes); - } - else - { - MXLogDebug(@"[ShareExtensionManager] Error with task_info(): %s", mach_error_string(kerr)); - } -} - - -#pragma mark - Notifications - -- (void)onMediaLoaderStateDidChange:(NSNotification *)notification -{ - MXMediaLoader *loader = (MXMediaLoader*)notification.object; - // Consider only upload progress - switch (loader.state) { - case MXMediaLoaderStateUploadInProgress: - { - self.imageUploadProgresses[loader.uploadId] = (NSNumber *)loader.statisticsDict[kMXMediaLoaderProgressValueKey]; - if ([self.delegate respondsToSelector:@selector(shareExtensionManager:mediaUploadProgress:)]) - { - const NSInteger totalImagesCount = self.pendingImages.count; - CGFloat totalProgress = 0.0; - - for (NSNumber *progress in self.imageUploadProgresses.allValues) - { - totalProgress += progress.floatValue/totalImagesCount; - } - - [self.delegate shareExtensionManager:self mediaUploadProgress:totalProgress]; - } - break; - } - default: - break; - } -} - -- (void)didReceiveMemoryWarning:(NSNotification*)notification -{ - MXLogDebug(@"[ShareExtensionManager] Did receive memory warning"); - [self logMemoryUsage]; -} - -#pragma mark - Sharing - -- (void)sendText:(NSString *)text toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock -{ - [self didStartSendingToRoom:room]; - if (!text) - { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - [room sendTextMessage:text success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } - } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendTextMessage failed."); - if (failureBlock) - { - failureBlock(error); - } - }]; -} - -- (void)sendFileWithUrl:(NSURL *)fileUrl toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock -{ - [self didStartSendingToRoom:room]; - if (!fileUrl) - { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - NSString *mimeType; - CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fileUrl pathExtension] , NULL); - mimeType = [self mimeTypeFromUTI:(__bridge NSString *)uti]; - CFRelease(uti); - - [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } - } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendFile failed."); - if (failureBlock) - { - failureBlock(error); - } - } keepActualFilename:YES]; -} - -- (void)sendImageData:(NSData *)imageData withProvider:(NSItemProvider*)itemProvider toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock -{ - [self didStartSendingToRoom:room]; - - NSString *imageUTI; - NSString *mimeType; - - // Try to get UTI plus mime type from NSItemProvider - imageUTI = [self utiFromImageTypeItemProvider:itemProvider]; - - if (imageUTI) - { - mimeType = [self mimeTypeFromUTI:imageUTI]; - } - - if (!mimeType) - { - // Try to get UTI plus mime type from image data - - imageUTI = [self utiFromImageData:imageData]; - - if (imageUTI) - { - mimeType = [self mimeTypeFromUTI:imageUTI]; - } - } - - // Sanity check - if (!mimeType) - { - MXLogDebug(@"[ShareExtensionManager] sendImage failed. Cannot determine MIME type of %@", itemProvider); - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - CGSize imageSize; - NSData *finalImageData; - - // Only resize JPEG or PNG files - if ([self isResizingSupportedForUTI:imageUTI]) - { - UIImage *convertedImage; - CGSize newImageSize; - - switch (self.imageCompressionMode) { - case ImageCompressionModeSmall: - newImageSize = CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE); - break; - case ImageCompressionModeMedium: - newImageSize = CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE); - break; - case ImageCompressionModeLarge: - newImageSize = CGSizeMake(self.actualLargeSize, self.actualLargeSize); - break; - default: - newImageSize = CGSizeZero; - break; - } - - if (CGSizeEqualToSize(newImageSize, CGSizeZero)) - { - // No resize to make - // Make sure the uploaded image orientation is up - if ([self isImageOrientationNotUpOrUndeterminedForImageData:imageData]) - { - UIImage *image = [UIImage imageWithData:imageData]; - convertedImage = [MXKTools forceImageOrientationUp:image]; - } - } - else - { - // Resize the image and set image in right orientation too - convertedImage = [MXKTools resizeImageWithData:imageData toFitInSize:newImageSize]; - } - - if (convertedImage) - { - if ([imageUTI isEqualToString:(__bridge NSString *)kUTTypePNG]) - { - finalImageData = UIImagePNGRepresentation(convertedImage); - } - else if ([imageUTI isEqualToString:(__bridge NSString *)kUTTypeJPEG]) - { - finalImageData = UIImageJPEGRepresentation(convertedImage, 0.9); - } - - imageSize = convertedImage.size; - } - else - { - finalImageData = imageData; - imageSize = [self imageSizeFromImageData:imageData]; - } - } - else - { - finalImageData = imageData; - imageSize = [self imageSizeFromImageData:imageData]; - } - - UIImage *thumbnail = nil; - // Thumbnail is useful only in case of encrypted room - if (room.summary.isEncrypted) - { - thumbnail = [MXKTools resizeImageWithData:imageData toFitInSize:CGSizeMake(800, 600)]; - } - - [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } - } failure:^(NSError *error) { - - MXLogDebug(@"[ShareExtensionManager] sendImage failed."); - if (failureBlock) - { - failureBlock(error); - } - - }]; -} - -- (void)sendImageDatas:(NSMutableArray *)imageDatas withProviders:(NSArray*)itemProviders toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock -{ - if (imageDatas.count == 0 || imageDatas.count != itemProviders.count) - { - MXLogDebug(@"[ShareExtensionManager] sendImages: no images to send."); - - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - [self didStartSendingToRoom:room]; - - dispatch_group_t requestsGroup = dispatch_group_create(); - __block NSError *firstRequestError; - - NSUInteger index = 0; - - for (NSData *imageData in imageDatas) - { - @autoreleasepool - { - dispatch_group_enter(requestsGroup); - - NSItemProvider *itemProvider = itemProviders[index]; - - [self sendImageData:imageData withProvider:itemProvider toRoom:room successBlock:^{ - dispatch_group_leave(requestsGroup); - } failureBlock:^(NSError *error) { - - if (error && !firstRequestError) - { - firstRequestError = error; - } - - dispatch_group_leave(requestsGroup); - }]; - } - - index++; - } - - dispatch_group_notify(requestsGroup, dispatch_get_main_queue(), ^{ - - if (firstRequestError) - { - if (failureBlock) - { - failureBlock(firstRequestError); - } - } - else - { - if (successBlock) - { - successBlock(); - } - } - }); -} - -- (void)sendVideo:(NSURL *)videoLocalUrl toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock -{ - AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil]; - - MXWeakify(self); - - // Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos. - UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString * _Nullable presetName) { - MXStrongifyAndReturnIfNil(self); - - // If the preset name is nil, the user cancelled. - if (!presetName) - { - return; - } - - // Set the chosen video conversion preset. - [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; - - [self didStartSendingToRoom:room]; - if (!videoLocalUrl) - { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - // Retrieve the video frame at 1 sec to define the video thumbnail - AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset]; - 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); - - [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } - } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendVideo failed."); - if (failureBlock) - { - failureBlock(error); - } - }]; - }]; - - [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; -} - - -@end - - -@implementation NSItemProvider (ShareExtensionManager) - -- (void)setIsLoaded:(BOOL)isLoaded -{ - NSNumber *number = @(isLoaded); - objc_setAssociatedObject(self, @selector(isLoaded), number, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)isLoaded -{ - NSNumber *number = objc_getAssociatedObject(self, @selector(isLoaded)); - return number.boolValue; -} - -@end diff --git a/RiotShareExtension/Modules/Main/SharePresentingViewController.m b/RiotShareExtension/Modules/Main/SharePresentingViewController.m deleted file mode 100644 index ef57eba3c..000000000 --- a/RiotShareExtension/Modules/Main/SharePresentingViewController.m +++ /dev/null @@ -1,79 +0,0 @@ -/* - Copyright 2017 Aram Sargsyan - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "SharePresentingViewController.h" -#import "ShareViewController.h" -#import "ShareExtensionManager.h" -#import "ThemeService.h" - -#ifdef IS_SHARE_EXTENSION -#import "RiotShareExtension-Swift.h" -#else -#import "Riot-Swift.h" -#endif - -@interface SharePresentingViewController () - -@property (nonatomic) ShareViewController *shareViewController; - -@end - -@implementation SharePresentingViewController - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - ShareExtensionManager *sharedManager = [ShareExtensionManager sharedManager]; - - sharedManager.primaryViewController = self; - sharedManager.shareExtensionContext = self.extensionContext; - - // Set up current theme - ThemeService.shared.themeId = RiotSettings.shared.userInterfaceTheme; - - [self presentShareViewController]; -} - -- (void)destroy -{ - if (self.shareViewController) - { - [self.shareViewController destroy]; - self.shareViewController = nil; - } -} - -- (void)presentShareViewController -{ - self.shareViewController = [[ShareViewController alloc] init]; - - self.shareViewController.providesPresentationContextTransitionStyle = YES; - self.shareViewController.definesPresentationContext = YES; - self.shareViewController.modalPresentationStyle = UIModalPresentationOverFullScreen; - self.shareViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - - [self presentViewController:self.shareViewController animated:YES completion:nil]; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - - -@end diff --git a/RiotShareExtension/Modules/Share/ShareViewController.h b/RiotShareExtension/Modules/Share/ShareViewController.h deleted file mode 100644 index 74e7af78a..000000000 --- a/RiotShareExtension/Modules/Share/ShareViewController.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - Copyright 2017 Aram Sargsyan - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import -#import - -@interface ShareViewController : MXKViewController - -@end diff --git a/RiotShareExtension/Modules/Share/ShareViewController.m b/RiotShareExtension/Modules/Share/ShareViewController.m deleted file mode 100644 index af4796e72..000000000 --- a/RiotShareExtension/Modules/Share/ShareViewController.m +++ /dev/null @@ -1,182 +0,0 @@ -/* - Copyright 2017 Aram Sargsyan - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "ShareViewController.h" -#import "SegmentedViewController.h" -#import "RoomsListViewController.h" -#import "FallbackViewController.h" -#import "ShareDataSource.h" -#import "ShareExtensionManager.h" - -#import "ThemeService.h" -#import "RiotShareExtension-Swift.h" - - -@interface ShareViewController () - -@property (weak, nonatomic) IBOutlet UIView *masterContainerView; -@property (weak, nonatomic) IBOutlet UILabel *titleLabel; -@property (weak, nonatomic) IBOutlet UIView *contentView; - -@property (nonatomic) SegmentedViewController *segmentedViewController; - -@property (nonatomic) id shareExtensionManagerDidUpdateAccountDataObserver; - - -@end - - -@implementation ShareViewController - -#pragma mark - Lifecycle - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.view.tintColor = ThemeService.shared.theme.tintColor; - self.titleLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - self.masterContainerView.backgroundColor = ThemeService.shared.theme.baseColor; - - self.shareExtensionManagerDidUpdateAccountDataObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kShareExtensionManagerDidUpdateAccountDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self configureViews]; - - }]; - - [self configureViews]; -} - -- (void)destroy -{ - [super destroy]; - - if (self.shareExtensionManagerDidUpdateAccountDataObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:self.shareExtensionManagerDidUpdateAccountDataObserver]; - self.shareExtensionManagerDidUpdateAccountDataObserver = nil; - } - - [self resetContentView]; -} - -- (void)resetContentView -{ - // Empty the content view - NSArray *subviews = self.contentView.subviews; - for (UIView *subview in subviews) - { - [subview removeFromSuperview]; - } - - // Release the current segmented view controller if any - if (self.segmentedViewController) - { - [self.segmentedViewController removeFromParentViewController]; - - // Release correctly all the existing data source and view controllers. - [self.segmentedViewController destroy]; - self.segmentedViewController = nil; - } -} - -#pragma mark - Private - -- (void)configureViews -{ - self.masterContainerView.layer.cornerRadius = 7; - - [self resetContentView]; - - if ([ShareExtensionManager sharedManager].userAccount) - { - self.titleLabel.text = [VectorL10n sendTo:@""]; - [self configureSegmentedViewController]; - } - else - { - NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary; - NSString *bundleDisplayName = infoDictionary[@"CFBundleDisplayName"]; - self.titleLabel.text = bundleDisplayName; - [self configureFallbackViewController]; - } -} - -- (void)configureSegmentedViewController -{ - self.segmentedViewController = [SegmentedViewController segmentedViewController]; - - NSArray *titles = @[[VectorL10n titleRooms], [VectorL10n titlePeople]]; - - void (^failureBlock)(void) = ^void() { - [self dismissViewControllerAnimated:YES completion:^{ - [[ShareExtensionManager sharedManager] terminateExtensionCanceled:NO]; - }]; - }; - - ShareDataSource *roomsDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModeRooms]; - RoomsListViewController *roomsViewController = [RoomsListViewController recentListViewController]; - roomsViewController.failureBlock = failureBlock; - [roomsViewController displayList:roomsDataSource]; - - ShareDataSource *peopleDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModePeople]; - RoomsListViewController *peopleViewController = [RoomsListViewController recentListViewController]; - peopleViewController.failureBlock = failureBlock; - [peopleViewController displayList:peopleDataSource]; - - [self.segmentedViewController initWithTitles:titles viewControllers:@[roomsViewController, peopleViewController] defaultSelected:0]; - - [self addChildViewController:self.segmentedViewController]; - [self.contentView addSubview:self.segmentedViewController.view]; - [self.segmentedViewController didMoveToParentViewController:self]; - - [self autoPinSubviewEdges:self.segmentedViewController.view toSuperviewEdges:self.contentView]; -} - -- (void)configureFallbackViewController -{ - FallbackViewController *fallbackVC = [FallbackViewController new]; - [self addChildViewController:fallbackVC]; - [self.contentView addSubview:fallbackVC.view]; - [fallbackVC didMoveToParentViewController:self]; - - [self autoPinSubviewEdges:fallbackVC.view toSuperviewEdges:self.contentView]; -} - -- (void)autoPinSubviewEdges:(UIView *)subview toSuperviewEdges:(UIView *)superview -{ - subview.translatesAutoresizingMaskIntoConstraints = NO; - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeWidth multiplier:1 constant:0]; - widthConstraint.active = YES; - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeHeight multiplier:1 constant:0]; - heightConstraint.active = YES; - NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]; - centerXConstraint.active = YES; - NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; - centerYConstraint.active = YES; -} - -#pragma mark - Actions - -- (IBAction)close:(UIButton *)sender -{ - [self dismissViewControllerAnimated:YES completion:^{ - [[ShareExtensionManager sharedManager] terminateExtensionCanceled:YES]; - }]; -} - - -@end diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h b/RiotShareExtension/Shared/ShareDataSource.h similarity index 51% rename from RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h rename to RiotShareExtension/Shared/ShareDataSource.h index 2a50bfdee..d030ec795 100644 --- a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h +++ b/RiotShareExtension/Shared/ShareDataSource.h @@ -16,23 +16,25 @@ #import -typedef NS_ENUM(NSInteger, ShareDataSourceMode) -{ - DataSourceModePeople, - DataSourceModeRooms -}; +@class ShareDataSource; +@protocol ShareDataSourceDelegate + +- (void)shareDataSourceDidChangeSelectedRoomIdentifiers:(ShareDataSource *)shareDataSource; + +@end @interface ShareDataSource : MXKRecentsDataSource -- (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode; +@property (nonatomic, weak) id shareDelegate; -/** - Returns the cell data at the index path - - @param indexPath the index of the cell - @return the MXKRecentCellData instance if it exists - */ -- (MXKRecentCellData *)cellDataAtIndexPath:(NSIndexPath *)indexPath; +@property (nonatomic, strong, readonly) NSSet *selectedRoomIdentifiers; + +- (instancetype)initWithFileStore:(MXFileStore *)fileStore + credentials:(MXCredentials *)credentials; + +- (void)selectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated; + +- (void)deselectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated; @end diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m similarity index 73% rename from RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m rename to RiotShareExtension/Shared/ShareDataSource.m index e841769ea..4bee2a44a 100644 --- a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m +++ b/RiotShareExtension/Shared/ShareDataSource.m @@ -15,26 +15,31 @@ */ #import "ShareDataSource.h" -#import "ShareExtensionManager.h" #import "RecentRoomTableViewCell.h" @interface ShareDataSource () -@property (nonatomic, readwrite) ShareDataSourceMode dataSourceMode; +@property (nonatomic, strong, readonly) MXFileStore *fileStore; +@property (nonatomic, strong, readonly) MXCredentials *credentials; @property NSArray *recentCellDatas; @property NSMutableArray *visibleRoomCellDatas; +@property (nonatomic, strong) NSMutableSet *internalSelectedRoomIdentifiers; + @end @implementation ShareDataSource -- (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode +- (instancetype)initWithFileStore:(MXFileStore *)fileStore + credentials:(MXCredentials *)credentials { - self = [super init]; - if (self) + if (self = [super init]) { - self.dataSourceMode = dataSourceMode; + _fileStore = fileStore; + _credentials = credentials; + + _internalSelectedRoomIdentifiers = [NSMutableSet set]; [self loadCellData]; } @@ -49,20 +54,39 @@ _visibleRoomCellDatas = nil; } +- (NSSet *)selectedRoomIdentifiers +{ + return self.internalSelectedRoomIdentifiers.copy; +} + +- (void)selectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated +{ + [self.internalSelectedRoomIdentifiers addObject:roomIdentifier]; + + [self.shareDelegate shareDataSourceDidChangeSelectedRoomIdentifiers:self]; +} + +- (void)deselectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated +{ + [self.internalSelectedRoomIdentifiers removeObject:roomIdentifier]; + + [self.shareDelegate shareDataSourceDidChangeSelectedRoomIdentifiers:self]; +} + #pragma mark - Private - (void)loadCellData { - [[ShareExtensionManager sharedManager].fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + [self.fileStore asyncRoomsSummaries:^(NSArray *roomsSummaries) { NSMutableArray *cellData = [NSMutableArray array]; // Add a fake matrix session to each room summary to provide it a REST client (used to handle correctly the room avatar). - MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:[ShareExtensionManager sharedManager].userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; + MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:self.credentials andOnUnrecognizedCertificateBlock:nil]]; for (MXRoomSummary *roomSummary in roomsSummaries) { - if (!roomSummary.hiddenFromUser && ((self.dataSourceMode == DataSourceModeRooms) ^ roomSummary.isDirect)) + if (!roomSummary.hiddenFromUser) { [roomSummary setMatrixSession:session]; @@ -133,6 +157,7 @@ { self.visibleRoomCellDatas = nil; } + [self.delegate dataSource:self didCellChange:nil]; } @@ -156,7 +181,11 @@ { RecentRoomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[RecentRoomTableViewCell defaultReuseIdentifier]]; - [cell render:[self cellDataAtIndexPath:indexPath]]; + MXKRecentCellData *data = [self cellDataAtIndexPath:indexPath]; + + [cell render:data]; + + [cell setCustomSelected:[self.selectedRoomIdentifiers containsObject:data.roomSummary.roomId] animated:NO]; return cell; } diff --git a/RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift b/RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift new file mode 100644 index 000000000..417fea577 --- /dev/null +++ b/RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift @@ -0,0 +1,35 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc public enum ShareItemType: UInt { + case fileURL, text, URL, image, video, movie, voiceMessage, unknown +} + +@objc public protocol ShareItemProtocol { + var type: ShareItemType { get } +} + +@objc public protocol ShareItemProviderProtocol { + var items: [ShareItemProtocol] { get } + + func areAllItemsImages() -> Bool + + func areAllItemsLoaded() -> Bool + + func loadItem(_ item: ShareItemProtocol, completion: @escaping (Any?, Error?) -> Void) +} diff --git a/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift b/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift new file mode 100644 index 000000000..e08435aec --- /dev/null +++ b/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift @@ -0,0 +1,109 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +private class SimpleShareItem: ShareItemProtocol { + let attachment: MXKAttachment? + let textMessage: String? + + init(withAttachment attachment: MXKAttachment) { + self.attachment = attachment + self.textMessage = nil + } + + init(withTextMessage textMessage: String) { + self.attachment = nil + self.textMessage = textMessage + } + + var type: ShareItemType { + guard textMessage == nil else { + return .text + } + + guard let attachment = attachment else { + return .unknown + } + + if attachment.type == MXKAttachmentTypeImage { + return .image + } else if attachment.type == MXKAttachmentTypeVideo { + return .video + } else if attachment.type == MXKAttachmentTypeFile { + return .fileURL + } else if attachment.type == MXKAttachmentTypeVoiceMessage { + return .voiceMessage + } else { + return .unknown + } + } +} + +@objc class SimpleShareItemProvider: NSObject, ShareItemProviderProtocol { + + private let attachment: MXKAttachment? + private let textMessage: String? + + let items: [ShareItemProtocol] + + private override init() { + attachment = nil + textMessage = nil + self.items = [] + } + + @objc public init(withAttachment attachment: MXKAttachment) { + self.attachment = attachment + self.items = [SimpleShareItem(withAttachment: attachment)]; + self.textMessage = nil + } + + @objc public init(withTextMessage textMessage: String) { + self.textMessage = textMessage + self.items = [SimpleShareItem(withTextMessage: textMessage)]; + self.attachment = nil + } + + func loadItem(_ item: ShareItemProtocol, completion: @escaping (Any?, Error?) -> Void) { + if let textMessage = self.textMessage { + completion(textMessage, nil) + return + } + + guard let attachment = attachment else { + fatalError("[SimpleShareItemProvider] Invalid item provider state.") + } + + attachment.prepareShare({ url in + DispatchQueue.main.async { + completion(url, nil) + } + }, failure: { error in + DispatchQueue.main.async { + completion(nil, error) + } + }) + } + + func areAllItemsLoaded() -> Bool { + return true + } + + func areAllItemsImages() -> Bool { + return (attachment != nil && attachment?.type == MXKAttachmentTypeImage) + } +} diff --git a/RiotShareExtension/Shared/ShareManager.h b/RiotShareExtension/Shared/ShareManager.h new file mode 100644 index 000000000..bb7245040 --- /dev/null +++ b/RiotShareExtension/Shared/ShareManager.h @@ -0,0 +1,46 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@protocol ShareItemProviderProtocol; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, ShareManagerType) { + ShareManagerTypeSend, + ShareManagerTypeForward, +}; + +typedef NS_ENUM(NSUInteger, ShareManagerResult) { + ShareManagerResultFinished, + ShareManagerResultCancelled, + ShareManagerResultFailed +}; + +@interface ShareManager : NSObject + +@property (nonatomic, copy) void (^completionCallback)(ShareManagerResult); + +- (instancetype)initWithShareItemProvider:(id)shareItemProvider + type:(ShareManagerType)type; + +- (UIViewController *)mainViewController; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m new file mode 100644 index 000000000..5ca23a2b1 --- /dev/null +++ b/RiotShareExtension/Shared/ShareManager.m @@ -0,0 +1,1094 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +@import MobileCoreServices; + +#import + +#import + +#import "ShareManager.h" +#import "ShareViewController.h" +#import "ShareDataSource.h" + +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif + +static const CGFloat kLargeImageSizeMaxDimension = 2048.0; +static const CGSize kThumbnailSize = {800.0, 600.0}; + +typedef NS_ENUM(NSInteger, ImageCompressionMode) +{ + ImageCompressionModeNone, + ImageCompressionModeSmall, + ImageCompressionModeMedium, + ImageCompressionModeLarge +}; + +@interface ShareManager () + +@property (nonatomic, strong, readonly) id shareItemProvider; +@property (nonatomic, strong, readonly) ShareViewController *shareViewController; + +@property (nonatomic, strong, readonly) NSMutableArray *pendingImages; +@property (nonatomic, strong, readonly) NSMutableDictionary *imageUploadProgresses; +@property (nonatomic, strong, readonly) id configuration; + +@property (nonatomic, strong) MXKAccount *userAccount; +@property (nonatomic, strong) MXFileStore *fileStore; + +@property (nonatomic, assign) ImageCompressionMode imageCompressionMode; +@property (nonatomic, assign) CGFloat actualLargeSize; + +@end + + +@implementation ShareManager + +- (instancetype)initWithShareItemProvider:(id)shareItemProvider + type:(ShareManagerType)type +{ + if (self = [super init]) + { + _shareItemProvider = shareItemProvider; + + _pendingImages = [NSMutableArray array]; + _imageUploadProgresses = [NSMutableDictionary dictionary]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaLoaderStateDidChange:) name:kMXMediaLoaderStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkUserAccount) name:kMXKAccountManagerDidRemoveAccountNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkUserAccount) name:NSExtensionHostWillEnterForegroundNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + + _configuration = [[CommonConfiguration alloc] init]; + [_configuration setupSettings]; + + // NSLog -> console.log file when not debugging the app + MXLogConfiguration *configuration = [[MXLogConfiguration alloc] init]; + configuration.logLevel = MXLogLevelVerbose; + configuration.logFilesSizeLimit = 0; + configuration.maxLogFilesCount = 10; + configuration.subLogName = @"share"; + + // Redirect NSLogs to files only if we are not debugging + if (!isatty(STDERR_FILENO)) { + configuration.redirectLogsToFiles = YES; + } + + [MXLog configure:configuration]; + + _shareViewController = [[ShareViewController alloc] initWithType:(type == ShareManagerTypeForward ? ShareViewControllerTypeForward : ShareViewControllerTypeSend) + currentState:ShareViewControllerAccountStateNotConfigured]; + [_shareViewController setDelegate:self]; + + // Set up runtime language on each context update. + NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; + NSString *language = [sharedUserDefaults objectForKey:@"appLanguage"]; + [NSBundle mxk_setLanguage:language]; + [NSBundle mxk_setFallbackLanguage:@"en"]; + + [self checkUserAccount]; + } + + return self; +} + +#pragma mark - Public + +- (UIViewController *)mainViewController +{ + return self.shareViewController; +} + +#pragma mark - ShareViewControllerDelegate + +- (void)shareViewController:(ShareViewController *)shareViewController didRequestShareForRoomIdentifiers:(NSSet *)roomIdentifiers +{ + MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:self.userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; + [MXFileStore setPreloadOptions:0]; + + MXWeakify(session); + [session setStore:self.fileStore success:^{ + MXStrongifyAndReturnIfNil(session); + + session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now + + NSMutableArray *rooms = [NSMutableArray array]; + for (NSString *roomIdentifier in roomIdentifiers) { + MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; + if (room) { + [rooms addObject:room]; + } + } + + [self sendContentToRooms:rooms success:^{ + self.completionCallback(ShareManagerResultFinished); + } failure:^(NSError *error){ + [self showFailureAlert:[VectorL10n roomEventFailedToSend]]; + }]; + + } failure:^(NSError *error) { + MXLogError(@"[ShareManager] Failed preparing matrix session"); + }]; +} + +- (void)shareViewControllerDidRequestDismissal:(ShareViewController *)shareViewController +{ + self.completionCallback(ShareManagerResultCancelled); +} + +#pragma mark - Private + +- (void)showFailureAlert:(NSString *)title +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; + + MXWeakify(self); + UIAlertAction *okAction = [UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + MXStrongifyAndReturnIfNil(self); + + if (self.completionCallback) + { + self.completionCallback(ShareManagerResultFailed); + } + }]; + + [alertController addAction:okAction]; + + [self.mainViewController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)sendContentToRooms:(NSArray *)rooms success:(void(^)(void))success failure:(void(^)(NSError *))failure +{ + [self resetPendingData]; + + __block NSError *firstRequestError = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + + void (^requestSuccess)(void) = ^() { + dispatch_group_leave(dispatchGroup); + }; + + void (^requestFailure)(NSError *) = ^(NSError *requestError) { + if (requestError && !firstRequestError) + { + firstRequestError = requestError; + } + + dispatch_group_leave(dispatchGroup); + }; + + MXWeakify(self); + for (id item in self.shareItemProvider.items) + { + if (item.type == ShareItemTypeText || item.type == ShareItemTypeURL) { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(id item, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + NSString *text = nil; + if([item isKindOfClass:[NSString class]]) + { + text = item; + } + else if([item isKindOfClass:[NSURL class]]) + { + text = [(NSURL *)item absoluteString]; + } + + if(text.length == 0) + { + requestFailure(nil); + return; + } + + [self sendText:text toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + } + + if (item.type == ShareItemTypeFileURL) { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *url, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + [self sendFileWithUrl:url toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + } + + if (item.type == ShareItemTypeVideo || item.type == ShareItemTypeMovie) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *videoLocalUrl, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + [self sendVideo:videoLocalUrl toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + } + + if (item.type == ShareItemTypeVoiceMessage) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *fileURL, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + [self sendVoiceMessage:fileURL toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + } + + if (item.type == ShareItemTypeImage) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(id itemProviderItem, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + NSData *imageData; + if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) + { + imageData = (NSData*)itemProviderItem; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) + { + NSURL *imageURL = (NSURL*)itemProviderItem; + imageData = [NSData dataWithContentsOfURL:imageURL]; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) + { + // An application can share directly an UIImage. + // The most common case is screenshot sharing without saving to file. + // As screenshot using PNG format when they are saved to file we also use PNG format when saving UIImage to NSData. + UIImage *image = (UIImage*)itemProviderItem; + imageData = UIImagePNGRepresentation(image); + } + + if (!imageData) + { + requestFailure(error); + return; + } + + if ([self.shareItemProvider areAllItemsImages]) + { + [self.pendingImages addObject:imageData]; + } + else + { + CGSize imageSize = [self imageSizeFromImageData:imageData]; + self.imageCompressionMode = ImageCompressionModeNone; + self.actualLargeSize = MAX(imageSize.width, imageSize.height); + + [self sendImageData:imageData toRooms:rooms success:requestSuccess failure:requestFailure]; + } + + // Only prompt for image resize if all items are images + // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. + if ([self.shareItemProvider areAllItemsImages]) + { + if ([self.shareItemProvider areAllItemsLoaded]) + { + UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ + [self sendImageDatas:self.pendingImages.copy toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + + if (compressionPrompt) + { + [self presentCompressionPrompt:compressionPrompt]; + } + } + else + { + dispatch_group_leave(dispatchGroup); + } + } + }]; + } + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + [self resetPendingData]; + + if (firstRequestError) + { + failure(firstRequestError); + } + else + { + success(); + } + }); +} + +- (void)checkUserAccount +{ + // Force account manager to reload account from the local storage. + [[MXKAccountManager sharedManager] forceReloadAccounts]; + + if (self.userAccount) + { + // Check whether the used account is still the first active one + MXKAccount *firstAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; + + // Compare the access token + if (!firstAccount || ![self.userAccount.mxCredentials.accessToken isEqualToString:firstAccount.mxCredentials.accessToken]) + { + // Remove this account + self.userAccount = nil; + } + } + + if (!self.userAccount) + { + // We consider the first enabled account. + // TODO: Handle multiple accounts + self.userAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; + } + + // Reset the file store to reload the room data. + if (_fileStore) + { + [_fileStore close]; + _fileStore = nil; + } + + if (self.userAccount) + { + _fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials]; + + ShareDataSource *roomDataSource = [[ShareDataSource alloc] initWithFileStore:_fileStore + credentials:self.userAccount.mxCredentials]; + + [self.shareViewController configureWithState:ShareViewControllerAccountStateConfigured + roomDataSource:roomDataSource]; + } else { + [self.shareViewController configureWithState:ShareViewControllerAccountStateNotConfigured + roomDataSource:nil]; + } +} + +- (void)resetPendingData +{ + [self.pendingImages removeAllObjects]; + [self.imageUploadProgresses removeAllObjects]; +} + +- (BOOL)isAPendingImageNotOrientedUp +{ + BOOL isAPendingImageNotOrientedUp = NO; + + for (NSData *imageData in self.pendingImages) + { + if ([self isImageOrientationNotUpOrUndeterminedForImageData:imageData]) + { + isAPendingImageNotOrientedUp = YES; + break; + } + } + + return isAPendingImageNotOrientedUp; +} + +// TODO: When select multiple images: +// - Enhance prompt to display sum of all file sizes for each compression. +// - Find a way to choose compression sizes for all images. +- (UIAlertController *)compressionPromptForPendingImagesWithShareBlock:(void(^)(void))shareBlock +{ + if (!self.pendingImages.count) + { + return nil; + } + + BOOL isAPendingImageNotOrientedUp = [self isAPendingImageNotOrientedUp]; + + NSData *firstImageData = self.pendingImages.firstObject; + UIImage *firstImage = [UIImage imageWithData:firstImageData]; + + MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:firstImage originalFileSize:firstImageData.length]; + + if (compressionSizes.small.fileSize == 0 && compressionSizes.medium.fileSize == 0 && compressionSizes.large.fileSize == 0) + { + if (isAPendingImageNotOrientedUp && self.pendingImages.count > 1) + { + self.imageCompressionMode = ImageCompressionModeSmall; + } + else + { + self.imageCompressionMode = ImageCompressionModeNone; + } + + MXLogDebug(@"[ShareManager] Send %lu image(s) without compression prompt using compression mode: %ld", (unsigned long)self.pendingImages.count, (long)self.imageCompressionMode); + + shareBlock(); + + return nil; + } + + UIAlertController *compressionPrompt = [UIAlertController alertControllerWithTitle:[MatrixKitL10n attachmentSizePromptTitle] + message:[MatrixKitL10n attachmentSizePromptMessage] + preferredStyle:UIAlertControllerStyleActionSheet]; + + if (compressionSizes.small.fileSize) + { + NSString *title = [MatrixKitL10n attachmentSmall:[MXTools fileSizeToString:compressionSizes.small.fileSize]]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeSmall; + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + if (compressionSizes.medium.fileSize) + { + NSString *title = [MatrixKitL10n attachmentMedium:[MXTools fileSizeToString:compressionSizes.medium.fileSize]]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeMedium; + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + // Do not offer the possibility to resize an image with a dimension above kLargeImageSizeMaxDimension, to prevent the risk of memory limit exception. + // TODO: Remove this condition when issue https://github.com/vector-im/riot-ios/issues/2341 will be fixed. + if (compressionSizes.large.fileSize && (MAX(compressionSizes.large.imageSize.width, compressionSizes.large.imageSize.height) <= kLargeImageSizeMaxDimension)) + { + NSString *title = [MatrixKitL10n attachmentLarge:[MXTools fileSizeToString:compressionSizes.large.fileSize]]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeLarge; + self.actualLargeSize = compressionSizes.actualLargeSize; + + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate + if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize) + { + NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize]; + + NSString *title = [MatrixKitL10n attachmentOriginal:fileSizeString]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeNone; + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + [compressionPrompt addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] + style:UIAlertActionStyleCancel + handler:nil]]; + + return compressionPrompt; +} + +- (void)didStartSending +{ + [self.shareViewController showProgressIndicator]; +} + +- (NSString*)utiFromImageData:(NSData*)imageData +{ + CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); + NSString *uti = (NSString*)CGImageSourceGetType(imageSource); + CFRelease(imageSource); + return uti; +} + +- (NSString*)mimeTypeFromUTI:(NSString*)uti +{ + return (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)uti, kUTTagClassMIMEType); +} + +- (BOOL)isResizingSupportedForImageData:(NSData*)imageData +{ + NSString *imageUTI = [self utiFromImageData:imageData]; + return [self isResizingSupportedForUTI:imageUTI]; +} + +- (BOOL)isResizingSupportedForUTI:(NSString*)imageUTI +{ + if ([imageUTI isEqualToString:(__bridge NSString *)kUTTypePNG] || [imageUTI isEqualToString:(__bridge NSString *)kUTTypeJPEG]) + { + return YES; + } + return NO; +} + +- (CGSize)imageSizeFromImageData:(NSData*)imageData +{ + CGFloat width = 0.0f; + CGFloat height = 0.0f; + + CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); + + CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); + + CFRelease(imageSource); + + if (imageProperties != NULL) + { + CFNumberRef widthNumber = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth); + CFNumberRef heightNumber = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight); + CFNumberRef orientationNumber = CFDictionaryGetValue(imageProperties, kCGImagePropertyOrientation); + + if (widthNumber != NULL) + { + CFNumberGetValue(widthNumber, kCFNumberCGFloatType, &width); + } + + if (heightNumber != NULL) + { + CFNumberGetValue(heightNumber, kCFNumberCGFloatType, &height); + } + + // Check orientation and flip size if required + if (orientationNumber != NULL) + { + int orientation; + CFNumberGetValue(orientationNumber, kCFNumberIntType, &orientation); + + // For orientation from kCGImagePropertyOrientationLeftMirrored to kCGImagePropertyOrientationLeft flip size + if (orientation >= 5) + { + CGFloat tempWidth = width; + width = height; + height = tempWidth; + } + } + + CFRelease(imageProperties); + } + + return CGSizeMake(width, height); +} + +- (NSNumber*)cgImageimageOrientationNumberFromImageData:(NSData*)imageData +{ + NSNumber *orientationNumber; + + CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); + + CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); + + CFRelease(imageSource); + + if (imageProperties != NULL) + { + CFNumberRef orientationNum = CFDictionaryGetValue(imageProperties, kCGImagePropertyOrientation); + + // Check orientation and flip size if required + if (orientationNum != NULL) + { + orientationNumber = (__bridge NSNumber *)orientationNum; + } + + CFRelease(imageProperties); + } + + return orientationNumber; +} + +- (BOOL)isImageOrientationNotUpOrUndeterminedForImageData:(NSData*)imageData +{ + BOOL isImageNotOrientedUp = YES; + + NSNumber *cgImageOrientationNumber = [self cgImageimageOrientationNumberFromImageData:imageData]; + + if (cgImageOrientationNumber && cgImageOrientationNumber.unsignedIntegerValue == (NSUInteger)kCGImagePropertyOrientationUp) + { + isImageNotOrientedUp = NO; + } + + return isImageNotOrientedUp; +} + +- (void)logCompressionSizeChoice:(MXKImageCompressionSize)compressionSize +{ + NSString *fileSize = [MXTools fileSizeToString:compressionSize.fileSize round:NO]; + NSUInteger imageWidth = compressionSize.imageSize.width; + NSUInteger imageHeight = compressionSize.imageSize.height; + + MXLogDebug(@"[ShareManager] User choose image compression with output size %lu x %lu (output file size: %@)", (unsigned long)imageWidth, (unsigned long)imageHeight, fileSize); + MXLogDebug(@"[ShareManager] Number of images to send: %lu", (unsigned long)self.pendingImages.count); +} + +// Log memory usage. +// NOTE: This result may not be reliable for all iOS versions (see https://forums.developer.apple.com/thread/64665 for more information). +- (void)logMemoryUsage +{ + struct task_basic_info basicInfo; + mach_msg_type_number_t size = TASK_BASIC_INFO_COUNT; + kern_return_t kerr = task_info(mach_task_self(), + TASK_BASIC_INFO, + (task_info_t)&basicInfo, + &size); + + vm_size_t memoryUsedInBytes = basicInfo.resident_size; + CGFloat memoryUsedInMegabytes = memoryUsedInBytes / (1024*1024); + + if (kerr == KERN_SUCCESS) + { + MXLogDebug(@"[ShareManager] Memory in use (in MB): %f", memoryUsedInMegabytes); + } + else + { + MXLogDebug(@"[ShareManager] Error with task_info(): %s", mach_error_string(kerr)); + } +} + +- (void)presentCompressionPrompt:(UIAlertController *)compressionPrompt +{ + [compressionPrompt popoverPresentationController].sourceView = self.mainViewController.view; + [compressionPrompt popoverPresentationController].sourceRect = self.mainViewController.view.frame; + [self.mainViewController presentViewController:compressionPrompt animated:YES completion:nil]; +} + +#pragma mark - Notifications + +- (void)onMediaLoaderStateDidChange:(NSNotification *)notification +{ + MXMediaLoader *loader = (MXMediaLoader*)notification.object; + // Consider only upload progress + switch (loader.state) { + case MXMediaLoaderStateUploadInProgress: + { + self.imageUploadProgresses[loader.uploadId] = (NSNumber *)loader.statisticsDict[kMXMediaLoaderProgressValueKey]; + + const NSInteger totalImagesCount = self.pendingImages.count; + CGFloat totalProgress = 0.0; + + for (NSNumber *progress in self.imageUploadProgresses.allValues) + { + totalProgress += progress.floatValue/totalImagesCount; + } + + [self.shareViewController setProgress:totalProgress]; + break; + } + default: + break; + } +} + +- (void)didReceiveMemoryWarning:(NSNotification*)notification +{ + MXLogDebug(@"[ShareManager] Did receive memory warning"); + [self logMemoryUsage]; +} + +#pragma mark - Sharing + +- (void)sendText:(NSString *)text + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + [self didStartSending]; + if (!text) + { + MXLogError(@"[ShareManager] Invalid text."); + failure(nil); + return; + } + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendTextMessage:text success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); + error = innerError; + dispatch_group_leave(dispatchGroup); + }]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); +} + +- (void)sendFileWithUrl:(NSURL *)fileUrl + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + [self didStartSending]; + if (!fileUrl) + { + MXLogError(@"[ShareManager] Invalid file url."); + failure(nil); + return; + } + + NSString *mimeType; + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fileUrl pathExtension] , NULL); + mimeType = [self mimeTypeFromUTI:(__bridge NSString *)uti]; + CFRelease(uti); + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendFile failed with error %@", innerError); + error = innerError; + dispatch_group_leave(dispatchGroup); + } keepActualFilename:YES]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); +} + +- (void)sendVideo:(NSURL *)videoLocalUrl + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil]; + + MXWeakify(self); + + // Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos. + UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString *presetName) { + MXStrongifyAndReturnIfNil(self); + + // If the preset name is nil, the user cancelled. + if (!presetName) + { + return; + } + + // Set the chosen video conversion preset. + [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; + + [self didStartSending]; + if (!videoLocalUrl) + { + MXLogError(@"[ShareManager] Invalid video file url."); + failure(nil); + return; + } + + // Retrieve the video frame at 1 sec to define the video thumbnail + AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset]; + 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); + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] Failed sending video with error %@", innerError); + error = innerError; + dispatch_group_leave(dispatchGroup); + }]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); + }]; + + [self presentCompressionPrompt:compressionPrompt]; +} + +- (void)sendVoiceMessage:(NSURL *)fileUrl + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + [self didStartSending]; + if (!fileUrl) + { + MXLogError(@"[ShareManager] Invalid voice message file url."); + failure(nil); + return; + } + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendVoiceMessage failed with error %@", error); + error = innerError; + dispatch_group_leave(dispatchGroup); + } keepActualFilename:YES]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); +} + +- (void)sendImageDatas:(NSArray> *)imageDatas + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + if (imageDatas.count == 0) + { + MXLogError(@"[ShareManager] sendImages: no images to send."); + failure(nil); + return; + } + + [self didStartSending]; + + dispatch_group_t requestsGroup = dispatch_group_create(); + __block NSError *firstRequestError; + + NSUInteger index = 0; + + for (NSData *imageData in imageDatas) + { + @autoreleasepool + { + dispatch_group_enter(requestsGroup); + [self sendImageData:imageData toRooms:rooms success:^{ + dispatch_group_leave(requestsGroup); + } failure:^(NSError *error) { + if (error && !firstRequestError) + { + firstRequestError = error; + } + + dispatch_group_leave(requestsGroup); + }]; + } + + index++; + } + + dispatch_group_notify(requestsGroup, dispatch_get_main_queue(), ^{ + + if (firstRequestError) + { + failure(firstRequestError); + } + else + { + success(); + } + }); +} + +- (void)sendImageData:(NSData *)imageData + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + [self didStartSending]; + + NSString *imageUTI; + NSString *mimeType; + + if (!mimeType) + { + imageUTI = [self utiFromImageData:imageData]; + if (imageUTI) + { + mimeType = [self mimeTypeFromUTI:imageUTI]; + } + } + + if (!mimeType) + { + MXLogError(@"[ShareManager] sendImage failed. Cannot determine MIME type ."); + if (failure) + { + failure(nil); + } + return; + } + + CGSize imageSize; + NSData *finalImageData; + + // Only resize JPEG or PNG files + if ([self isResizingSupportedForUTI:imageUTI]) + { + UIImage *convertedImage; + CGSize newImageSize; + + switch (self.imageCompressionMode) { + case ImageCompressionModeSmall: + newImageSize = CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE); + break; + case ImageCompressionModeMedium: + newImageSize = CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE); + break; + case ImageCompressionModeLarge: + newImageSize = CGSizeMake(self.actualLargeSize, self.actualLargeSize); + break; + default: + newImageSize = CGSizeZero; + break; + } + + if (CGSizeEqualToSize(newImageSize, CGSizeZero)) + { + // No resize to make + // Make sure the uploaded image orientation is up + if ([self isImageOrientationNotUpOrUndeterminedForImageData:imageData]) + { + UIImage *image = [UIImage imageWithData:imageData]; + convertedImage = [MXKTools forceImageOrientationUp:image]; + } + } + else + { + // Resize the image and set image in right orientation too + convertedImage = [MXKTools resizeImageWithData:imageData toFitInSize:newImageSize]; + } + + if (convertedImage) + { + if ([imageUTI isEqualToString:(__bridge NSString *)kUTTypePNG]) + { + finalImageData = UIImagePNGRepresentation(convertedImage); + } + else if ([imageUTI isEqualToString:(__bridge NSString *)kUTTypeJPEG]) + { + finalImageData = UIImageJPEGRepresentation(convertedImage, 0.9); + } + + imageSize = convertedImage.size; + } + else + { + finalImageData = imageData; + imageSize = [self imageSizeFromImageData:imageData]; + } + } + else + { + finalImageData = imageData; + imageSize = [self imageSizeFromImageData:imageData]; + } + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + + UIImage *thumbnail = nil; + if (room.summary.isEncrypted) // Thumbnail is useful only in case of encrypted room + { + thumbnail = [MXKTools resizeImageWithData:imageData toFitInSize:kThumbnailSize]; + } + + dispatch_group_enter(dispatchGroup); + [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendImage failed with error %@", error); + error = innerError; + dispatch_group_leave(dispatchGroup); + }]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); +} + +@end diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.h b/RiotShareExtension/Shared/View/FallbackViewController.h similarity index 87% rename from RiotShareExtension/Modules/Fallback/FallbackViewController.h rename to RiotShareExtension/Shared/View/FallbackViewController.h index ddee2a064..c4f26c2ef 100644 --- a/RiotShareExtension/Modules/Fallback/FallbackViewController.h +++ b/RiotShareExtension/Shared/View/FallbackViewController.h @@ -14,8 +14,8 @@ limitations under the License. */ -#import +@import UIKit; -@interface FallbackViewController : MXKViewController +@interface FallbackViewController : UIViewController @end diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.m b/RiotShareExtension/Shared/View/FallbackViewController.m similarity index 90% rename from RiotShareExtension/Modules/Fallback/FallbackViewController.m rename to RiotShareExtension/Shared/View/FallbackViewController.m index 74f81b4a0..57412f705 100644 --- a/RiotShareExtension/Modules/Fallback/FallbackViewController.m +++ b/RiotShareExtension/Shared/View/FallbackViewController.m @@ -42,10 +42,4 @@ self.logoImageView.tintColor = ThemeService.shared.theme.tintColor; } -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - @end diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.xib b/RiotShareExtension/Shared/View/FallbackViewController.xib similarity index 100% rename from RiotShareExtension/Modules/Fallback/FallbackViewController.xib rename to RiotShareExtension/Shared/View/FallbackViewController.xib diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.h b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h similarity index 91% rename from RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.h rename to RiotShareExtension/Shared/View/RecentRoomTableViewCell.h index 959ca786e..63eb213f7 100644 --- a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.h +++ b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h @@ -20,4 +20,6 @@ + (CGFloat)cellHeight; +- (void)setCustomSelected:(BOOL)selected animated:(BOOL)animated; + @end diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m similarity index 78% rename from RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m rename to RiotShareExtension/Shared/View/RecentRoomTableViewCell.m index 90550a99e..4dec0815a 100644 --- a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m +++ b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m @@ -18,13 +18,19 @@ #import "MXRoomSummary+Riot.h" #import "ThemeService.h" + +#ifdef IS_SHARE_EXTENSION #import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif @interface RecentRoomTableViewCell () @property (weak, nonatomic) IBOutlet MXKImageView *avatarImageView; @property (weak, nonatomic) IBOutlet UILabel *roomTitleLabel; @property (weak, nonatomic) IBOutlet UIImageView *encryptedRoomIcon; +@property (weak, nonatomic) IBOutlet UIButton *selectionButton; @end @@ -51,6 +57,12 @@ self.roomTitleLabel.textColor = ThemeService.shared.theme.textPrimaryColor; self.contentView.backgroundColor = ThemeService.shared.theme.backgroundColor; + + [self.selectionButton setImage:[UIImage imageNamed:@"radio-button-default"] forState:UIControlStateNormal]; + [self.selectionButton setImage:[UIImage imageNamed:@"radio-button-selected"] forState:UIControlStateSelected]; + + [self.selectionButton setTitle:@"" forState:UIControlStateNormal]; + [self.selectionButton setTitle:@"" forState:UIControlStateSelected]; } - (void)layoutSubviews @@ -87,4 +99,11 @@ return 74; } +- (void)setCustomSelected:(BOOL)selected animated:(BOOL)animated +{ + [UIView animateWithDuration:(animated ? 0.25f : 0.0f) animations:^{ + [self.selectionButton setSelected:selected]; + }]; +} + @end diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.xib b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib similarity index 72% rename from RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.xib rename to RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib index 80e07581e..fe39d2a67 100644 --- a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.xib +++ b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -25,12 +25,9 @@ - + diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h b/RiotShareExtension/Shared/View/RoomsListViewController.h similarity index 94% rename from RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h rename to RiotShareExtension/Shared/View/RoomsListViewController.h index b8a50f7e4..bee26845b 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h +++ b/RiotShareExtension/Shared/View/RoomsListViewController.h @@ -18,8 +18,8 @@ #import "MXRoom+Riot.h" #import "ShareDataSource.h" +@class RoomsListViewController; + @interface RoomsListViewController : MXKRecentListViewController -@property (copy) void (^failureBlock)(void); - @end diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m b/RiotShareExtension/Shared/View/RoomsListViewController.m similarity index 59% rename from RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m rename to RiotShareExtension/Shared/View/RoomsListViewController.m index 3f6e67b2e..414e0364a 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m +++ b/RiotShareExtension/Shared/View/RoomsListViewController.m @@ -14,13 +14,13 @@ limitations under the License. */ +#import + #import "RoomsListViewController.h" #import "RecentRoomTableViewCell.h" -#import "NSBundle+MatrixKit.h" -#import "ShareExtensionManager.h" +#import "ShareDataSource.h" #import "RecentCellData.h" #import "ThemeService.h" -#import #ifdef IS_SHARE_EXTENSION #import "RiotShareExtension-Swift.h" @@ -28,9 +28,7 @@ #import "Riot-Swift.h" #endif -@interface RoomsListViewController () - -@property (nonatomic) MXKPieChartHUD *hudView; +@interface RoomsListViewController () // The fake search bar displayed at the top of the recents table. We switch on the actual search bar (self.recentsSearchBar) // when the user selects it. @@ -136,76 +134,6 @@ return; } -#pragma mark - Private - -- (void)showShareAlertForRoomPath:(NSIndexPath *)indexPath -{ - MXKRecentCellData *recentCellData = [self.dataSource cellDataAtIndexPath:indexPath]; - NSString *roomName = recentCellData.roomSummary.displayname; - if (!roomName.length) - { - roomName = [MatrixKitL10n roomDisplaynameEmptyRoom]; - } - - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[VectorL10n sendTo:roomName] message:nil preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[MatrixKitL10n cancel] style:UIAlertActionStyleCancel handler:nil]; - [alertController addAction:cancelAction]; - - UIAlertAction *sendAction = [UIAlertAction actionWithTitle:[MatrixKitL10n send] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - - // The selected room is instanciated here - MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:[ShareExtensionManager sharedManager].userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; - - [MXFileStore setPreloadOptions:0]; - - MXWeakify(session); - [session setStore:[ShareExtensionManager sharedManager].fileStore success:^{ - MXStrongifyAndReturnIfNil(session); - - MXRoom *selectedRoom = [MXRoom loadRoomFromStore:[ShareExtensionManager sharedManager].fileStore withRoomId:recentCellData.roomSummary.roomId matrixSession:session]; - - // Do not warn for unknown devices. We have cross-signing now - session.crypto.warnOnUnknowDevices = NO; - - [ShareExtensionManager sharedManager].delegate = self; - - [[ShareExtensionManager sharedManager] sendContentToRoom:selectedRoom failureBlock:^(NSError* error) { - - NSString *title; - if ([error.domain isEqualToString:MXEncryptingErrorDomain]) - { - title = [VectorL10n shareExtensionFailedToEncrypt]; - } - - [self showFailureAlert:title]; - }]; - - } failure:^(NSError *error) { - - MXLogDebug(@"[RoomsListViewController] failed to prepare matrix session]"); - - }]; - }]; - - [alertController addAction:sendAction]; - - [self presentViewController:alertController animated:YES completion:nil]; -} - -- (void)showFailureAlert:(NSString *)title -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title.length ? title : [VectorL10n roomEventFailedToSend] message:nil preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *okAction = [UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - if (self.failureBlock) - { - self.failureBlock(); - } - }]; - [alertController addAction:okAction]; - [self presentViewController:alertController animated:YES completion:nil]; -} - #pragma mark - UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath @@ -217,7 +145,16 @@ { [tableView deselectRowAtIndexPath:indexPath animated:YES]; - [self showShareAlertForRoomPath:indexPath]; + NSString *roomIdentifier = [self.dataSource cellDataAtIndexPath:indexPath].roomSummary.roomId; + + ShareDataSource *dataSource = (ShareDataSource *)self.dataSource; + if ([dataSource.selectedRoomIdentifiers containsObject:roomIdentifier]) { + [dataSource deselectRoomWithIdentifier:roomIdentifier animated:YES]; + } else { + [dataSource selectRoomWithIdentifier:roomIdentifier animated:YES]; + } + + [self.recentsTableView reloadData]; } #pragma mark - MXKDataSourceDelegate @@ -304,34 +241,4 @@ } } -#pragma mark - ShareExtensionManagerDelegate - -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager showImageCompressionPrompt:(UIAlertController *)compressionPrompt -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [compressionPrompt popoverPresentationController].sourceView = self.view; - [compressionPrompt popoverPresentationController].sourceRect = self.view.frame; - [self presentViewController:compressionPrompt animated:YES completion:nil]; - }); -} - -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager didStartSendingContentToRoom:(MXRoom *)room -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (!self.hudView) - { - self.parentViewController.view.userInteractionEnabled = NO; - self.hudView = [MXKPieChartHUD showLoadingHudOnView:self.view WithMessage:[VectorL10n sending]]; - [self.hudView setProgress:0.0]; - } - }); -} - -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager mediaUploadProgress:(CGFloat)progress -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [self.hudView setProgress:progress]; - }); -} - @end diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib b/RiotShareExtension/Shared/View/RoomsListViewController.xib similarity index 100% rename from RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib rename to RiotShareExtension/Shared/View/RoomsListViewController.xib diff --git a/RiotShareExtension/Shared/View/ShareViewController.h b/RiotShareExtension/Shared/View/ShareViewController.h new file mode 100644 index 000000000..d45a66049 --- /dev/null +++ b/RiotShareExtension/Shared/View/ShareViewController.h @@ -0,0 +1,57 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +@import UIKit; + +@class ShareViewController; +@class ShareDataSource; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, ShareViewControllerType) { + ShareViewControllerTypeSend, + ShareViewControllerTypeForward +}; + +typedef NS_ENUM(NSUInteger, ShareViewControllerAccountState) { + ShareViewControllerAccountStateConfigured, + ShareViewControllerAccountStateNotConfigured +}; + +@protocol ShareViewControllerDelegate + +- (void)shareViewController:(ShareViewController *)shareViewController didRequestShareForRoomIdentifiers:(NSSet *)roomIdentifiers; +- (void)shareViewControllerDidRequestDismissal:(ShareViewController *)shareViewController; + +@end + +@interface ShareViewController : UIViewController + +@property (nonatomic, weak, nullable) id delegate; + +- (instancetype)initWithType:(ShareViewControllerType)type + currentState:(ShareViewControllerAccountState)state; + +- (void)configureWithState:(ShareViewControllerAccountState)state + roomDataSource:(nullable ShareDataSource *)roomDataSource; + +- (void)showProgressIndicator; + +- (void)setProgress:(CGFloat)progress; + +@end + +NS_ASSUME_NONNULL_END diff --git a/RiotShareExtension/Shared/View/ShareViewController.m b/RiotShareExtension/Shared/View/ShareViewController.m new file mode 100644 index 000000000..cfe4f2a87 --- /dev/null +++ b/RiotShareExtension/Shared/View/ShareViewController.m @@ -0,0 +1,193 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "ShareViewController.h" +#import "ShareDataSource.h" +#import "RoomsListViewController.h" +#import "FallbackViewController.h" + +#import "ThemeService.h" + +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif + +@interface ShareViewController () + +@property (nonatomic, assign, readonly) ShareViewControllerType type; + +@property (nonatomic, assign) ShareViewControllerAccountState state; + +@property (nonatomic, strong) RoomsListViewController *roomListViewController; +@property (nonatomic, strong) ShareDataSource *roomDataSource; + +@property (nonatomic, strong) FallbackViewController *fallbackViewController; + +@property (nonatomic, weak) IBOutlet UIView *masterContainerView; +@property (nonatomic, weak) IBOutlet UIButton *cancelButton; +@property (nonatomic, weak) IBOutlet UILabel *titleLabel; +@property (nonatomic, weak) IBOutlet UIButton *shareButton; +@property (nonatomic, weak) IBOutlet UIView *contentView; + +@property (nonatomic, strong) MXKPieChartHUD *hudView; + +@end + + +@implementation ShareViewController + +- (instancetype)initWithType:(ShareViewControllerType)type + currentState:(ShareViewControllerAccountState)state +{ + if (self = [super init]) + { + _type = type; + _state = state; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.masterContainerView setBackgroundColor:ThemeService.shared.theme.baseColor]; + [self.masterContainerView.layer setCornerRadius:7.0]; + + [self.titleLabel setTextColor:ThemeService.shared.theme.textPrimaryColor]; + + [self.cancelButton setTintColor:ThemeService.shared.theme.tintColor]; + [self.cancelButton setTitle:[VectorL10n cancel] forState:UIControlStateNormal]; + + [self.shareButton setTintColor:ThemeService.shared.theme.tintColor]; + [self.shareButton setEnabled:NO]; + + [self configureWithState:self.state roomDataSource:self.roomDataSource]; +} + +- (void)configureWithState:(ShareViewControllerAccountState)state + roomDataSource:(ShareDataSource *)roomDataSource +{ + self.state = state; + self.roomDataSource = roomDataSource; + self.roomDataSource.shareDelegate = self; + + if (!self.isViewLoaded) { + return; + } + + [self configureViews]; +} + +- (void)showProgressIndicator +{ + if (!self.hudView) + { + self.parentViewController.view.userInteractionEnabled = NO; + self.hudView = [MXKPieChartHUD showLoadingHudOnView:self.view WithMessage:[VectorL10n sending]]; + [self.hudView setProgress:0.0]; + } +} + +- (void)setProgress:(CGFloat)progress +{ + [self.hudView setProgress:progress]; +} + +#pragma mark - ShareDataSourceDelegate + +- (void)shareDataSourceDidChangeSelectedRoomIdentifiers:(ShareDataSource *)shareDataSource +{ + self.shareButton.enabled = (shareDataSource.selectedRoomIdentifiers.count > 0); +} + +#pragma mark - Private + +- (void)configureViews +{ + [self resetContentView]; + + if (self.state == ShareViewControllerAccountStateConfigured) + { + [self configureSegmentedViewController]; + [self.shareButton setHidden:NO]; + + if (self.type == ShareViewControllerTypeSend) { + [self.titleLabel setText:[VectorL10n sendTo:@""]]; + [self.shareButton setTitle:[VectorL10n sendTo:@""] forState:UIControlStateNormal]; + } else { + [self.titleLabel setText:[VectorL10n roomEventActionForward]]; + [self.shareButton setTitle:[VectorL10n roomEventActionForward] forState:UIControlStateNormal]; + } + } + else + { + [self configureFallbackViewController]; + [self.shareButton setHidden:NO]; + + self.titleLabel.text = [AppInfo.current displayName]; + } +} + +- (void)configureSegmentedViewController +{ + self.roomListViewController = [RoomsListViewController recentListViewController]; + [self.roomListViewController displayList:self.roomDataSource]; + + [self addChildViewController:self.roomListViewController]; + [self.contentView vc_addSubViewMatchingParent:self.roomListViewController.view]; + [self.roomListViewController didMoveToParentViewController:self]; +} + +- (void)configureFallbackViewController +{ + self.fallbackViewController = [FallbackViewController new]; + [self addChildViewController:self.fallbackViewController]; + [self.contentView vc_addSubViewMatchingParent:self.fallbackViewController.view]; + [self.fallbackViewController didMoveToParentViewController:self]; +} + +- (void)resetContentView +{ + [self.roomListViewController willMoveToParentViewController:nil]; + [self.roomListViewController.view removeFromSuperview]; + [self.roomListViewController removeFromParentViewController]; + + [self.fallbackViewController willMoveToParentViewController:nil]; + [self.fallbackViewController.view removeFromSuperview]; + [self.fallbackViewController removeFromParentViewController]; +} + +#pragma mark - Actions + +- (IBAction)onCancelButtonTap:(UIButton *)sender +{ + [self.delegate shareViewControllerDidRequestDismissal:self]; +} + +- (IBAction)onShareButtonTap:(UIButton *)sender +{ + if (self.roomDataSource.selectedRoomIdentifiers.count == 0) { + return; + } + + [self.delegate shareViewController:self didRequestShareForRoomIdentifiers:self.roomDataSource.selectedRoomIdentifiers]; +} + +@end diff --git a/RiotShareExtension/Modules/Share/ShareViewController.xib b/RiotShareExtension/Shared/View/ShareViewController.xib similarity index 59% rename from RiotShareExtension/Modules/Share/ShareViewController.xib rename to RiotShareExtension/Shared/View/ShareViewController.xib index 04a1316c8..ece4f812b 100644 --- a/RiotShareExtension/Modules/Share/ShareViewController.xib +++ b/RiotShareExtension/Shared/View/ShareViewController.xib @@ -1,16 +1,18 @@ - + - + + + @@ -21,40 +23,51 @@ - + - + - + - + + + + + - - + + + + - + @@ -64,9 +77,7 @@ - - @@ -74,21 +85,12 @@ - - - - - - - - - - + + + + - - - diff --git a/RiotShareExtension/Modules/Main/SharePresentingViewController.h b/RiotShareExtension/Sources/ShareExtensionRootViewController.h similarity index 88% rename from RiotShareExtension/Modules/Main/SharePresentingViewController.h rename to RiotShareExtension/Sources/ShareExtensionRootViewController.h index c9eff12d0..e0868b6ad 100644 --- a/RiotShareExtension/Modules/Main/SharePresentingViewController.h +++ b/RiotShareExtension/Sources/ShareExtensionRootViewController.h @@ -16,8 +16,6 @@ #import -@interface SharePresentingViewController : UIViewController - -- (void)destroy; +@interface ShareExtensionRootViewController : UIViewController @end diff --git a/RiotShareExtension/Sources/ShareExtensionRootViewController.m b/RiotShareExtension/Sources/ShareExtensionRootViewController.m new file mode 100644 index 000000000..aee019949 --- /dev/null +++ b/RiotShareExtension/Sources/ShareExtensionRootViewController.m @@ -0,0 +1,84 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "ShareExtensionRootViewController.h" +#import "ShareManager.h" +#import "ThemeService.h" + +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif + +@interface ShareExtensionRootViewController () + +@property (nonatomic, strong, readonly) ShareManager *shareManager; + +@end + +@implementation ShareExtensionRootViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [ThemeService.shared setThemeId:RiotSettings.shared.userInterfaceTheme]; + + ShareExtensionShareItemProvider *provider = [[ShareExtensionShareItemProvider alloc] initWithExtensionContext:self.extensionContext]; + _shareManager = [[ShareManager alloc] initWithShareItemProvider:provider type:ShareManagerTypeSend]; + + MXWeakify(self); + [_shareManager setCompletionCallback:^(ShareManagerResult result) { + MXStrongifyAndReturnIfNil(self); + + switch (result) + { + case ShareManagerResultFinished: + [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; + [self dismiss]; + break; + case ShareManagerResultCancelled: + [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; + [self dismiss]; + break; + case ShareManagerResultFailed: + [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; + [self dismiss]; + break; + default: + break; + } + }]; + + [self.shareManager.mainViewController setModalInPopover:YES]; + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; +} + +#pragma mark - Private + +- (void)dismiss +{ + [self dismissViewControllerAnimated:true completion:^{ + [self.presentingViewController dismissViewControllerAnimated:false completion:nil]; + + // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. + // For now, we force the share extension to exit and free memory. + [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; + }]; +} + +@end diff --git a/RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift b/RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift new file mode 100644 index 000000000..df80ab563 --- /dev/null +++ b/RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift @@ -0,0 +1,130 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MobileCoreServices +import MatrixKit + +private class ShareExtensionItem: ShareItemProtocol { + let itemProvider: NSItemProvider + + var loaded = false + + init(itemProvider: NSItemProvider) { + self.itemProvider = itemProvider + } + + var type: ShareItemType { + if itemProvider.hasItemConformingToTypeIdentifier(MXKUTI.text.rawValue) { + return .text + } else if itemProvider.hasItemConformingToTypeIdentifier(MXKUTI.url.rawValue) { + return .URL + } else if itemProvider.hasItemConformingToTypeIdentifier(MXKUTI.fileUrl.rawValue) { + return .fileURL + } else if itemProvider.hasItemConformingToTypeIdentifier(MXKUTI.image.rawValue) { + return .image + } else if itemProvider.hasItemConformingToTypeIdentifier(MXKUTI.video.rawValue) { + return .video + } else if itemProvider.hasItemConformingToTypeIdentifier(MXKUTI.movie.rawValue) { + return .movie + } + + return .unknown + } +} + +@objcMembers +class ShareExtensionShareItemProvider: NSObject, ShareItemProviderProtocol { + + public let items: [ShareItemProtocol] + + public init(extensionContext: NSExtensionContext) { + + var items: [ShareItemProtocol] = [] + for case let extensionItem as NSExtensionItem in extensionContext.inputItems { + guard let attachments = extensionItem.attachments else { + continue; + } + + for itemProvider in attachments { + items.append(ShareExtensionItem(itemProvider: itemProvider)) + } + } + self.items = items + } + + func areAllItemsLoaded() -> Bool { + for case let item as ShareExtensionItem in self.items { + if !item.loaded { + return false + } + } + + return true + } + + func areAllItemsImages() -> Bool { + for case let item as ShareExtensionItem in self.items { + if item.type != .image { + return false + } + } + + return true + } + + func loadItem(_ item: ShareItemProtocol, completion: @escaping (Any?, Error?) -> Void) { + guard let shareExtensionItem = item as? ShareExtensionItem else { + fatalError("[ShareExtensionShareItemProvider] Unexpected item type.") + } + + let typeIdentifier = typeIdentifierForType(item.type) + + shareExtensionItem.loaded = false + shareExtensionItem.itemProvider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { result, error in + if error == nil { + shareExtensionItem.loaded = true + } + + DispatchQueue.main.async { + completion(result, error) + } + } + } + + // MARK: - Private + + private func typeIdentifierForType(_ type: ShareItemType) -> String { + switch type { + case .text: + return MXKUTI.text.rawValue + case .URL: + return MXKUTI.url.rawValue + case .fileURL: + return MXKUTI.fileUrl.rawValue + case .image: + return MXKUTI.image.rawValue + case .video: + return MXKUTI.video.rawValue + case .movie: + return MXKUTI.movie.rawValue + case .voiceMessage: + return MXKUTI.fileUrl.rawValue + default: + return "" + } + } +} diff --git a/RiotShareExtension/SupportingFiles/Info.plist b/RiotShareExtension/SupportingFiles/Info.plist index f0a876166..fce6e4c3f 100644 --- a/RiotShareExtension/SupportingFiles/Info.plist +++ b/RiotShareExtension/SupportingFiles/Info.plist @@ -43,7 +43,7 @@ NSExtensionPointIdentifier com.apple.share-services NSExtensionPrincipalClass - SharePresentingViewController + ShareExtensionRootViewController diff --git a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h index d3bf536b2..9d1bfc7d9 100644 --- a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h +++ b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h @@ -4,3 +4,4 @@ #import "ThemeService.h" #import "AvatarGenerator.h" +#import "BuildInfo.h" diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 9e61389f1..81e952ff5 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -52,9 +52,11 @@ targets: - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift - path: ../Riot/Managers/KeyValueStorage - path: ../Riot/Managers/Settings/RiotSettings.swift + - path: ../Riot/Managers/AppInfo/ - path: ../Riot/Categories/UIColor.swift - path: ../Riot/Categories/UISearchBar.swift - path: ../Riot/Categories/String.swift + - path: ../Riot/Categories/UIView.swift - path: ../Riot/Modules/Common/Recents/CellData/RecentCellData.m - path: ../Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift - path: ../Riot/Generated/Strings.swift diff --git a/changelog.d/5009.feature b/changelog.d/5009.feature new file mode 100644 index 000000000..55e904a3b --- /dev/null +++ b/changelog.d/5009.feature @@ -0,0 +1 @@ +Implemented message forwarding from within the main application. Updated the share extension designs. \ No newline at end of file