diff --git a/android/app/build.gradle b/android/app/build.gradle index 2fbb0e576..3b7934374 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,7 +106,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion 21 targetSdkVersion 23 - versionCode 81 + versionCode 82 versionName "1.6.0" multiDexEnabled true ndk { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 57cd38df8..b82c71425 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2022,7 +2022,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/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index bdfdae302..9eb4c05b6 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -2400,7 +2400,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 81; + CURRENT_PROJECT_VERSION = 82; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -2449,7 +2449,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 81; + CURRENT_PROJECT_VERSION = 82; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 0e7958018..1d95b85ae 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 81 + 82 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 4e9e402ca..fe9bf0879 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -23,7 +23,7 @@ CFBundleShortVersionString 1.6.0 CFBundleVersion - 81 + 82 NSAppTransportSecurity NSAllowsArbitraryLoads 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/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist index e8fa092cf..2221397f4 100644 --- a/ios/MattermostTests/Info.plist +++ b/ios/MattermostTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 81 + 82 diff --git a/share_extension/ios/extension_channels.js b/share_extension/ios/extension_channels.js index 13ad5fb94..fcd1e911c 100644 --- a/share_extension/ios/extension_channels.js +++ b/share_extension/ios/extension_channels.js @@ -119,28 +119,51 @@ export default class ExtensionChannels extends PureComponent { const {currentUserId, teamId} = this.props; const channelsMap = {}; + // get the channels for the specified team const myChannels = await Client4.getMyChannels(teamId); - const myPreferences = await Client4.getMyPreferences(); - const usersInDms = myPreferences.filter((pref) => pref.category === Preferences.CATEGORY_DIRECT_CHANNEL_SHOW).map((pref) => pref.name); + // filter channels that are direct and group messages const dms = myChannels.filter((channel) => { - const teammateId = getUserIdFromChannelName(currentUserId, channel.name); - return (channel.type === General.DM_CHANNEL && usersInDms.includes(teammateId)) || channel.type === General.GM_CHANNEL; + return channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL; }); - const dmProfiles = await Client4.getProfilesByIds(usersInDms); + const usersInDms = dms.filter((channel) => { + return channel.type === General.DM_CHANNEL; + }).map((channel) => { + return getUserIdFromChannelName(currentUserId, channel.name); + }).reduce((acc, teammateId) => { + if (!acc.includes(teammateId)) { + acc.push(teammateId); + } + + return acc; + }, []); + + // get the user ids that belong to DMs & GMs + let dmProfiles; + if (usersInDms.length) { + try { + dmProfiles = await Client4.getProfilesByIds(usersInDms); + } catch (e) { + // do nothing + } + } for (let i = 0; i < dms.length; i++) { const channel = dms[i]; - if (channel.type === General.DM_CHANNEL) { + if (channel.type === General.DM_CHANNEL && dmProfiles) { const teammateId = getUserIdFromChannelName(currentUserId, channel.name); const profile = dmProfiles.find((p) => p.id === teammateId); channelsMap[channel.id] = displayUsername(profile, Preferences.DISPLAY_PREFER_FULL_NAME); } else if (channel.type === General.GM_CHANNEL) { - const members = await Client4.getChannelMembers(channel.id, 0, General.MAX_USERS_IN_GM); - const userIds = members.filter((m) => m.user_id !== currentUserId).map((m) => m.user_id); - const gmProfiles = await Client4.getProfilesByIds(userIds); - channelsMap[channel.id] = this.getGroupDisplayNameFromUserIds(userIds, gmProfiles); + try { + const members = await Client4.getChannelMembers(channel.id, 0, General.MAX_USERS_IN_GM); + const userIds = members.filter((m) => m.user_id !== currentUserId).map((m) => m.user_id); + const gmProfiles = await Client4.getProfilesByIds(userIds); + channelsMap[channel.id] = this.getGroupDisplayNameFromUserIds(userIds, gmProfiles); + } catch (e) { + // do nothing + } } } diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js index 2ce26f2b0..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; @@ -220,7 +230,8 @@ export default class ExtensionPost extends PureComponent { Client4.setUrl(credentials.url); Client4.setToken(credentials.token); - this.setState({channel, currentUserId, files, team, value}); + Client4.setUserId(currentUserId); + this.setState({channel, currentUserId, files, team, value, totalSize}); } catch (error) { this.setState({error}); } @@ -243,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 ( @@ -292,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; } @@ -405,7 +447,7 @@ export default class ExtensionPost extends PureComponent { numberOfLines={1} style={styles.filename} > - {file.filename} + {`${file.size} - ${file.filename}`} ); @@ -415,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; } @@ -484,6 +530,7 @@ export default class ExtensionPost extends PureComponent { requestId: generateId() }; + this.setState({sending: true}); onClose(data); } catch (error) { this.setState({error}); @@ -496,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); @@ -506,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)} @@ -644,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/extension_teams.js b/share_extension/ios/extension_teams.js index c9c841b25..0a1479d4b 100644 --- a/share_extension/ios/extension_teams.js +++ b/share_extension/ios/extension_teams.js @@ -57,14 +57,15 @@ export default class ExtensionTeams extends PureComponent { const myMembers = await Client4.getMyTeamMembers(); const myTeams = []; - teams.forEach(async (team) => { + for (let i = 0; i < teams.length; i++) { + const team = teams[i]; const belong = myMembers.find((member) => member.team_id === team.id); if (belong) { const channels = await Client4.getMyChannels(team.id); defaultChannels[team.id] = channels.find((channel) => channel.name === General.DEFAULT_CHANNEL); myTeams.push(team); } - }); + } this.setState({ defaultChannels, 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) => {