From a3d9eaa385520d103ae3bfcf3664b09d1d07ee0d Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 30 Jan 2018 13:27:20 -0300 Subject: [PATCH] iOS Share extension to share files up to 20mb (#1394) --- assets/base/i18n/en.json | 2 + ios/MattermostShare/PerformRequests.h | 4 +- ios/MattermostShare/PerformRequests.m | 11 +++- ios/MattermostShare/ShareViewController.m | 10 +-- share_extension/ios/extension_post.js | 76 ++++++++++++++++++++--- share_extension/ios/index.js | 17 +---- 6 files changed, 87 insertions(+), 33 deletions(-) diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index d5ba2d273..d436fdde8 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2014,7 +2014,9 @@ "mobile.error_handler.title": "Unexpected error occurred", "mobile.extension.authentication_required": "Authentication required: Please first login using the app.", "mobile.extension.file_limit": "Sharing is limited to a maximum of 5 files.", + "mobile.extension.max_file_size": "File attachments shared in Mattermost must be less than 20 Mb.", "mobile.extension.permission": "Mattermost needs access to the device storage to share files.", + "mobile.extension.posting": "Posting...", "mobile.extension.title": "Share in Mattermost", "mobile.file_upload.camera": "Take Photo or Video", "mobile.file_upload.library": "Photo Library", diff --git a/ios/MattermostShare/PerformRequests.h b/ios/MattermostShare/PerformRequests.h index 17edcfa3f..98b91d150 100644 --- a/ios/MattermostShare/PerformRequests.h +++ b/ios/MattermostShare/PerformRequests.h @@ -17,12 +17,14 @@ @property (nonatomic, strong) NSString *serverUrl; @property (nonatomic, strong) NSString *token; +@property (nonatomic, strong) NSExtensionContext *extensionContext; @property NSUserDefaults *bucket; - (id) initWithPost:(NSDictionary *) post withFiles:(NSArray *) files forRequestId:(NSString *)requestId - inAppGroupId:(NSString *) appGroupId; + inAppGroupId:(NSString *) appGroupId + inContext:(NSExtensionContext *) extensionContext; -(void)createPost; @end diff --git a/ios/MattermostShare/PerformRequests.m b/ios/MattermostShare/PerformRequests.m index b3d9819b0..4e6d76215 100644 --- a/ios/MattermostShare/PerformRequests.m +++ b/ios/MattermostShare/PerformRequests.m @@ -16,13 +16,15 @@ MattermostBucket *mattermostBucket; - (id) initWithPost:(NSDictionary *) post withFiles:(NSArray *) files forRequestId:(NSString *)requestId - inAppGroupId:(NSString *) appGroupId { + inAppGroupId:(NSString *) appGroupId + inContext:(NSExtensionContext *) context { self = [super init]; if (self) { self.post = post; self.files = files; self.appGroupId = appGroupId; self.requestId = requestId; + self.extensionContext = context; mattermostBucket = [[MattermostBucket alloc] init]; self.bucket = [mattermostBucket bucketByName: appGroupId]; @@ -42,6 +44,8 @@ MattermostBucket *mattermostBucket; -(void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(nullable NSError *)error { if(error != nil) { NSLog(@"ERROR %@", [error userInfo]); + [self.extensionContext completeRequestReturningItems:nil + completionHandler:nil]; } NSLog(@"invalidating session %@", self.requestId); [session finishTasksAndInvalidate]; @@ -103,7 +107,7 @@ MattermostBucket *mattermostBucket; [dataForm appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", POST_BODY_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]]; [uploadRequest setHTTPBody:dataForm]; - NSURLSession *uploadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; + NSURLSession *uploadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionDataTask *uploadTask = [uploadSession dataTaskWithRequest:uploadRequest]; NSLog(@"Executing file request"); [uploadTask resume]; @@ -132,6 +136,9 @@ MattermostBucket *mattermostBucket; NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; NSLog(@"Executing post request"); [createTask resume]; + [self.extensionContext completeRequestReturningItems:nil + completionHandler:nil]; + NSLog(@"Extension closed"); } - (void) cleanUpTempFiles { diff --git a/ios/MattermostShare/ShareViewController.m b/ios/MattermostShare/ShareViewController.m index ed2532f49..b75c9d763 100644 --- a/ios/MattermostShare/ShareViewController.m +++ b/ios/MattermostShare/ShareViewController.m @@ -54,13 +54,13 @@ RCT_EXPORT_METHOD(close:(NSDictionary *)data appGroupId:(NSString *)appGroupId) NSArray *files = [data objectForKey:@"files"]; NSString *requestId = [data objectForKey:@"requestId"]; NSLog(@"Call createPost"); - PerformRequests *request = [[PerformRequests alloc] initWithPost:post withFiles:files forRequestId:requestId inAppGroupId:appGroupId]; + PerformRequests *request = [[PerformRequests alloc] initWithPost:post withFiles:files forRequestId:requestId inAppGroupId:appGroupId inContext:extensionContext]; [request createPost]; - } - - [extensionContext completeRequestReturningItems:nil + } else { + [extensionContext completeRequestReturningItems:nil completionHandler:nil]; - NSLog(@"Extension closed"); + NSLog(@"Extension closed"); + } } RCT_REMAP_METHOD(data, diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js index dc45bc000..1ff4db49d 100644 --- a/share_extension/ios/extension_post.js +++ b/share_extension/ios/extension_post.js @@ -5,6 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import { + ActivityIndicator, Dimensions, Image, NativeModules, @@ -17,9 +18,10 @@ import { import IonIcon from 'react-native-vector-icons/Ionicons'; import Video from 'react-native-video'; import LocalAuth from 'react-native-local-auth'; +import RNFetchBlob from 'react-native-fetch-blob'; import {Client4} from 'mattermost-redux/client'; -import {lookupMimeType} from 'mattermost-redux/utils/file_utils'; +import {getFormattedFileSize, lookupMimeType} from 'mattermost-redux/utils/file_utils'; import mattermostBucket from 'app/mattermost_bucket'; import {generateId} from 'app/utils/file'; @@ -42,6 +44,7 @@ import ExtensionTeams from './extension_teams'; const ShareExtension = NativeModules.MattermostShare; const MAX_INPUT_HEIGHT = 95; const MAX_MESSAGE_LENGTH = 4000; +const MAX_FILE_SIZE = 20 * 1024 * 1024; const extensionSvg = { csv: ExcelSvg, @@ -76,7 +79,9 @@ export default class ExtensionPost extends PureComponent { error: null, files: [], isLandscape, - value: '' + totalSize: 0, + value: '', + sending: false }; } @@ -185,6 +190,7 @@ export default class ExtensionPost extends PureComponent { const text = []; const urls = []; const files = []; + let totalSize = 0; for (let i = 0; i < items.length; i++) { const item = items[i]; @@ -198,14 +204,18 @@ export default class ExtensionPost extends PureComponent { default: { const fullPath = item.value; const filePath = fullPath.replace('file://', ''); + const fileSize = await RNFetchBlob.fs.stat(filePath); const filename = fullPath.replace(/^.*[\\/]/, ''); const extension = filename.split('.').pop(); + + totalSize += fileSize.size; files.push({ extension, filename, filePath, fullPath, mimeType: lookupMimeType(filename.toLowerCase()), + size: getFormattedFileSize(fileSize), type: item.type }); break; @@ -221,7 +231,7 @@ export default class ExtensionPost extends PureComponent { Client4.setUrl(credentials.url); Client4.setToken(credentials.token); Client4.setUserId(currentUserId); - this.setState({channel, currentUserId, files, team, value}); + this.setState({channel, currentUserId, files, team, value, totalSize}); } catch (error) { this.setState({error}); } @@ -244,7 +254,34 @@ export default class ExtensionPost extends PureComponent { renderBody = (styles) => { const {formatMessage} = this.context.intl; const {credentials, theme} = this.props; - const {error, value} = this.state; + const {error, sending, totalSize, value} = this.state; + + if (sending) { + return ( + + + + {formatMessage({ + id: 'mobile.extension.posting', + defaultMessage: 'Posting...' + })} + + + ); + } + + if (totalSize >= MAX_FILE_SIZE) { + return ( + + + {formatMessage({ + id: 'mobile.extension.max_file_size', + defaultMessage: 'File attachments shared in Mattermost must be less than 20 Mb.' + })} + + + ); + } if (credentials && !error) { return ( @@ -293,9 +330,13 @@ export default class ExtensionPost extends PureComponent { renderChannelButton = (styles) => { const {formatMessage} = this.context.intl; const {credentials, theme} = this.props; - const {channel} = this.state; + const {channel, sending} = this.state; const channelName = channel ? channel.display_name : ''; + if (sending) { + return null; + } + if (!credentials) { return null; } @@ -406,7 +447,7 @@ export default class ExtensionPost extends PureComponent { numberOfLines={1} style={styles.filename} > - {file.filename} + {`${file.size} - ${file.filename}`} ); @@ -416,9 +457,13 @@ export default class ExtensionPost extends PureComponent { renderTeamButton = (styles) => { const {formatMessage} = this.context.intl; const {credentials, theme} = this.props; - const {team} = this.state; + const {sending, team} = this.state; const teamName = team ? team.display_name : ''; + if (sending) { + return null; + } + if (!credentials) { return null; } @@ -485,6 +530,7 @@ export default class ExtensionPost extends PureComponent { requestId: generateId() }; + this.setState({sending: true}); onClose(data); } catch (error) { this.setState({error}); @@ -497,6 +543,7 @@ export default class ExtensionPost extends PureComponent { render() { const {credentials, theme} = this.props; + const {totalSize, sending} = this.state; const {formatMessage} = this.context.intl; const styles = getStyleSheet(theme); @@ -507,10 +554,10 @@ export default class ExtensionPost extends PureComponent { > = MAX_FILE_SIZE || sending) ? null : formatMessage({id: 'mobile.share_extension.send', defaultMessage: 'Send'})} theme={theme} /> {this.renderBody(styles)} @@ -645,6 +692,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { justifyContent: 'center', overflow: 'hidden', width: 38 + }, + sendingContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + paddingHorizontal: 15 + }, + sendingText: { + color: theme.centerChannelColor, + fontSize: 16, + paddingTop: 10 } }; }); diff --git a/share_extension/ios/index.js b/share_extension/ios/index.js index 229390e1f..86105796b 100644 --- a/share_extension/ios/index.js +++ b/share_extension/ios/index.js @@ -61,22 +61,7 @@ export default class SharedApp extends PureComponent { } onClose = (data) => { - Animated.parallel([ - Animated.timing( - this.state.backdropOpacity, - { - toValue: 0, - duration: 250 - }), - Animated.timing( - this.state.containerOpacity, - { - toValue: 0, - duration: 250 - }) - ]).start(() => { - ShareExtension.close(data, Config.AppGroupId); - }); + ShareExtension.close(data, Config.AppGroupId); }; onLayout = (e) => {