diff --git a/android/app/build.gradle b/android/app/build.gradle index 3b7934374..e3abf4de3 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 82 + versionCode 84 versionName "1.6.0" multiDexEnabled true ndk { diff --git a/app/mattermost.js b/app/mattermost.js index d0ef66517..5345ac694 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -199,7 +199,7 @@ export default class Mattermost { const intl = this.getIntl(); if (isSecured) { try { - mattermostBucket.set('emm', vendor, LocalConfig.AppGroupId); + mattermostBucket.setPreference('emm', vendor, LocalConfig.AppGroupId); await mattermostManaged.authenticate({ reason: intl.formatMessage({ id: 'mobile.managed.secured_by', diff --git a/app/mattermost_bucket/index.js b/app/mattermost_bucket/index.js index a858cc00b..70da1d49c 100644 --- a/app/mattermost_bucket/index.js +++ b/app/mattermost_bucket/index.js @@ -7,14 +7,14 @@ import {NativeModules, Platform} from 'react-native'; const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucket : null; export default { - set: (key, value, groupName) => { + setPreference: (key, value, groupName) => { if (MattermostBucket) { - MattermostBucket.set(key, value, groupName); + MattermostBucket.setPreference(key, value, groupName); } }, - get: async (key, groupName) => { + getPreference: async (key, groupName) => { if (MattermostBucket) { - const value = await MattermostBucket.get(key, groupName); + const value = await MattermostBucket.getPreference(key, groupName); if (value) { try { return JSON.parse(value); @@ -26,9 +26,33 @@ export default { return null; }, - remove: (key, groupName) => { + removePreference: (key, groupName) => { if (MattermostBucket) { - MattermostBucket.remove(key, groupName); + MattermostBucket.removePreference(key, groupName); + } + }, + writeToFile: (fileName, content, groupName) => { + if (MattermostBucket) { + MattermostBucket.writeToFile(fileName, content, groupName); + } + }, + readFromFile: async (fileName, groupName) => { + if (MattermostBucket) { + const value = await MattermostBucket.readFromFile(fileName, groupName); + if (value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } + } + } + + return null; + }, + removeFile: (fileName, groupName) => { + if (MattermostBucket) { + MattermostBucket.removeFile(fileName, groupName); } } }; diff --git a/app/store/index.js b/app/store/index.js index daef76094..9e61ec17a 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -2,7 +2,7 @@ // See License.txt for license information. import {batchActions} from 'redux-batched-actions'; -import {AsyncStorage} from 'react-native'; +import {AsyncStorage, Platform} from 'react-native'; import {createBlacklistFilter} from 'redux-persist-transform-filter'; import {createTransform, persistStore} from 'redux-persist'; @@ -13,10 +13,14 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {NavigationTypes, ViewTypes} from 'app/constants'; import appReducer from 'app/reducers'; +import {throttle} from 'app/utils/general'; import networkConnectionListener from 'app/utils/network'; import {createSentryMiddleware} from 'app/utils/sentry/middleware'; import {promiseTimeout} from 'app/utils/promise_timeout'; +import mattermostBucket from 'app/mattermost_bucket'; +import Config from 'assets/config'; + import {messageRetention, shareExtensionData} from './middleware'; import {transformSet} from './utils'; @@ -130,6 +134,16 @@ export default function configureAppStore(initialState) { let purging = false; + // for iOS write the entities to a shared file + if (Platform.OS === 'ios') { + store.subscribe(throttle(() => { + const state = store.getState(); + if (state.entities) { + mattermostBucket.writeToFile('entities', JSON.stringify(state.entities), Config.AppGroupId); + } + }, 1000)); + } + // check to see if the logout request was successful store.subscribe(async () => { const state = store.getState(); diff --git a/app/store/middleware.js b/app/store/middleware.js index c67ca9626..3969a6db0 100644 --- a/app/store/middleware.js +++ b/app/store/middleware.js @@ -3,11 +3,7 @@ import DeviceInfo from 'react-native-device-info'; -import {ChannelTypes, GeneralTypes, TeamTypes, UserTypes} from 'mattermost-redux/action_types'; -import {General} from 'mattermost-redux/constants'; -import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences'; -import {getUserIdFromChannelName, getGroupDisplayNameFromUserIds} from 'mattermost-redux/utils/channel_utils'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {UserTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; import initialState from 'app/initial_state'; @@ -314,92 +310,15 @@ function cleanupState(action, keepCurrent = false) { }; } -export function shareExtensionData(store) { +export function shareExtensionData() { return (next) => (action) => { // allow other middleware to do their things const nextAction = next(action); switch (action.type) { - case 'persist/REHYDRATE': { - const {entities} = action.payload; - if (entities) { - if (entities.general && entities.general.credentials && entities.general.credentials.token) { - mattermostBucket.set('credentials', JSON.stringify(entities.general.credentials), Config.AppGroupId); - } - - if (entities.teams) { - const {currentTeamId, teams} = entities.teams; - if (currentTeamId) { - const team = teams[currentTeamId]; - const teamToSave = { - id: currentTeamId, - name: team.name, - display_name: team.display_name - }; - mattermostBucket.set('selectedTeam', JSON.stringify(teamToSave), Config.AppGroupId); - } - } - - if (entities.users) { - const {currentUserId} = entities.users; - if (currentUserId) { - mattermostBucket.set('currentUserId', currentUserId, Config.AppGroupId); - } - } - } - break; - } - case GeneralTypes.RECEIVED_APP_CREDENTIALS: - mattermostBucket.set('credentials', JSON.stringify(action.data), Config.AppGroupId); - break; - case ChannelTypes.SELECT_CHANNEL: { - const state = store.getState(); - const {channels} = state.entities.channels; - const {currentUserId, profiles, profilesInChannel} = state.entities.users; - const channel = {...channels[action.data]}; - if (channel.type === General.DM_CHANNEL) { - const teammateId = getUserIdFromChannelName(currentUserId, channel.name); - channel.display_name = displayUsername(profiles[teammateId], getTeammateNameDisplaySetting(state)); - } else if (channel.type === General.GM_CHANNEL) { - channel.display_name = getGroupDisplayNameFromUserIds( - profilesInChannel[channel.id], - profiles, - currentUserId, - getTeammateNameDisplaySetting(state) - ); - } - - const channelToSave = { - id: channel.id, - name: channel.name, - display_name: channel.display_name, - type: channel.type - }; - mattermostBucket.set('selectedChannel', JSON.stringify(channelToSave), Config.AppGroupId); - break; - } - case 'BATCH_SELECT_TEAM': { - const teamData = action.payload.find((data) => data.type === TeamTypes.SELECT_TEAM); - if (teamData && teamData.data) { - const team = store.getState().entities.teams.teams[teamData.data]; - const teamToSave = { - id: team.id, - name: team.name, - display_name: team.display_name - }; - mattermostBucket.set('selectedTeam', JSON.stringify(teamToSave), Config.AppGroupId); - } - break; - } - case UserTypes.RECEIVED_ME: - mattermostBucket.set('currentUserId', action.data.id, Config.AppGroupId); - break; case UserTypes.LOGOUT_SUCCESS: - mattermostBucket.remove('credentials', Config.AppGroupId); - mattermostBucket.remove('selectedChannel', Config.AppGroupId); - mattermostBucket.remove('selectedTeam', Config.AppGroupId); - mattermostBucket.remove('currentUserId', Config.AppGroupId); - mattermostBucket.remove('emm', Config.AppGroupId); + mattermostBucket.removePreference('emm', Config.AppGroupId); + mattermostBucket.removeFile('entities', Config.AppGroupId); break; } return nextAction; diff --git a/app/utils/general.js b/app/utils/general.js index b7df87a9f..aa81a3e5a 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -35,3 +35,25 @@ export function alertErrorIfInvalidPermissions(result) { export function emptyFunction() { return; } + +export function throttle(fn, limit, ...args) { + let inThrottle; + let lastFunc; + let lastRan; + + return () => { + if (inThrottle) { + clearTimeout(lastFunc); + lastFunc = setTimeout(() => { + if ((Date.now() - lastRan) >= limit) { + Reflect.apply(fn, this, args); + lastRan = Date.now(); + } + }, limit - (Date.now() - lastRan)); + } else { + Reflect.apply(fn, this, args); + lastRan = Date.now(); + inThrottle = true; + } + }; +} diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 9eb4c05b6..d7733d519 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 = 82; + CURRENT_PROJECT_VERSION = 84; 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 = 82; + CURRENT_PROJECT_VERSION = 84; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 1d95b85ae..31d7b95da 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 82 + 84 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostBucket.h b/ios/MattermostBucket.h index 1e43783f5..79bda4b8a 100644 --- a/ios/MattermostBucket.h +++ b/ios/MattermostBucket.h @@ -3,4 +3,5 @@ @interface MattermostBucket : NSObject - (NSUserDefaults *)bucketByName:(NSString*)name; +-(NSString *)readFromFile:(NSString *)fileName appGroupId:(NSString *)appGroupId; @end diff --git a/ios/MattermostBucket.m b/ios/MattermostBucket.m index 8772850b0..13358e70b 100644 --- a/ios/MattermostBucket.m +++ b/ios/MattermostBucket.m @@ -10,32 +10,25 @@ @implementation MattermostBucket -- (NSUserDefaults *)bucketByName:(NSString*)name { - return [[NSUserDefaults alloc] initWithSuiteName: name]; -} - -+ (BOOL)requiresMainQueueSetup ++(BOOL)requiresMainQueueSetup { return YES; } RCT_EXPORT_MODULE(); -RCT_EXPORT_METHOD(set:(NSString *) key - value:(NSString *) value - bucketName:(NSString*) bucketName) -{ - NSUserDefaults* bucket = [self bucketByName: bucketName]; - [bucket setObject:value forKey:key]; +RCT_EXPORT_METHOD(writeToFile:(NSString *)fileName + content:(NSString *)content + bucketName:(NSString *)bucketName) { + [self writeToFile:fileName content:content appGroupId:bucketName]; } -RCT_EXPORT_METHOD(get:(NSString *) key - bucketName:(NSString*) bucketName +RCT_EXPORT_METHOD(readFromFile:(NSString *)fileName + bucketName:(NSString*)bucketName getWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - NSUserDefaults* bucket = [self bucketByName: bucketName]; - id value = [bucket objectForKey:key]; + id value = [self readFromFile:fileName appGroupId:bucketName]; if (value == nil) { value = [NSNull null]; @@ -44,11 +37,86 @@ RCT_EXPORT_METHOD(get:(NSString *) key resolve(value); } -RCT_EXPORT_METHOD(remove:(NSString *) key - bucketName:(NSString*) bucketName) +RCT_EXPORT_METHOD(removeFile:(NSString *)fileName + bucketName:(NSString*)bucketName) { - NSUserDefaults* bucket = [self bucketByName: bucketName]; - [bucket removeObjectForKey: key]; + [self removeFile:fileName appGroupId:bucketName]; } +RCT_EXPORT_METHOD(setPreference:(NSString *) key + value:(NSString *) value + bucketName:(NSString*) bucketName) +{ + [self setPreference:key value:value appGroupId:bucketName]; +} + +RCT_EXPORT_METHOD(getPreference:(NSString *) key + bucketName:(NSString*) bucketName + getWithResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + id value = [self getPreference:key appGroupId:bucketName]; + + if (value == nil) { + value = [NSNull null]; + } + + resolve(value); +} + +RCT_EXPORT_METHOD(removePreference:(NSString *) key + bucketName:(NSString*) bucketName) +{ + [self removePreference:key appGroupId:bucketName]; +} + +-(NSString *)fileUrl:(NSString *)fileName appGroupdId:(NSString *)appGroupId { + NSURL *fileManagerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId]; + return [NSString stringWithFormat:@"%@/%@", fileManagerURL.path, fileName]; +} + +-(void)writeToFile:(NSString *)fileName content:(NSString *)content appGroupId:(NSString *)appGroupId { + NSString *filePath = [self fileUrl:fileName appGroupdId:appGroupId]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if(![fileManager fileExistsAtPath:filePath]) { + [fileManager createFileAtPath:filePath contents:nil attributes:nil]; + } + [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil]; +} + +-(NSString *)readFromFile:(NSString *)fileName appGroupId:(NSString *)appGroupId { + NSString *filePath = [self fileUrl:fileName appGroupdId:appGroupId]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if(![fileManager fileExistsAtPath:filePath]) { + return nil; + } + return [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; +} + +-(void)removeFile:(NSString *)fileName appGroupId:(NSString *)appGroupId { + NSString *filePath = [self fileUrl:fileName appGroupdId:appGroupId]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if([fileManager isDeletableFileAtPath:filePath]) { + [fileManager removeItemAtPath:filePath error:nil]; + } +} + +-(NSUserDefaults *)bucketByName:(NSString*)name { + return [[NSUserDefaults alloc] initWithSuiteName: name]; +} + +-(void) setPreference:(NSString *)key value:(NSString *) value appGroupId:(NSString*)appGroupId { + NSUserDefaults* bucket = [self bucketByName: appGroupId]; + [bucket setObject:value forKey:key]; +} + +-(id) getPreference:(NSString *)key appGroupId:(NSString*)appGroupId { + NSUserDefaults* bucket = [self bucketByName: appGroupId]; + return [bucket objectForKey:key]; +} + +-(void) removePreference:(NSString *)key appGroupId:(NSString*)appGroupId { + NSUserDefaults* bucket = [self bucketByName: appGroupId]; + [bucket removeObjectForKey: key]; +} @end diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index fe9bf0879..d85f93606 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -23,7 +23,7 @@ CFBundleShortVersionString 1.6.0 CFBundleVersion - 82 + 84 NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/MattermostShare/PerformRequests.h b/ios/MattermostShare/PerformRequests.h index 98b91d150..471c90213 100644 --- a/ios/MattermostShare/PerformRequests.h +++ b/ios/MattermostShare/PerformRequests.h @@ -7,6 +7,7 @@ // #import +#import "MattermostBucket.h" @interface PerformRequests : NSObject @property (nonatomic, strong) NSString *appGroupId; @@ -18,7 +19,7 @@ @property (nonatomic, strong) NSString *serverUrl; @property (nonatomic, strong) NSString *token; @property (nonatomic, strong) NSExtensionContext *extensionContext; -@property NSUserDefaults *bucket; +@property MattermostBucket *bucket; - (id) initWithPost:(NSDictionary *) post withFiles:(NSArray *) files diff --git a/ios/MattermostShare/PerformRequests.m b/ios/MattermostShare/PerformRequests.m index 4e6d76215..8cc3639b9 100644 --- a/ios/MattermostShare/PerformRequests.m +++ b/ios/MattermostShare/PerformRequests.m @@ -11,7 +11,6 @@ #import "ShareViewController.h" @implementation PerformRequests -MattermostBucket *mattermostBucket; - (id) initWithPost:(NSDictionary *) post withFiles:(NSArray *) files @@ -26,17 +25,17 @@ MattermostBucket *mattermostBucket; self.requestId = requestId; self.extensionContext = context; - mattermostBucket = [[MattermostBucket alloc] init]; - self.bucket = [mattermostBucket bucketByName: appGroupId]; + self.bucket = [[MattermostBucket alloc] init]; [self setCredentials]; } return self; } -(void)setCredentials { - NSString *credentialsString = [self.bucket objectForKey:@"credentials"]; - NSData *credentialsData = [credentialsString dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *credentials = [NSJSONSerialization JSONObjectWithData:credentialsData options:NSJSONReadingMutableContainers error:nil]; + NSString *entitiesString = [self.bucket readFromFile:@"entities" appGroupId:self.appGroupId]; + NSData *entitiesData = [entitiesString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *entities = [NSJSONSerialization JSONObjectWithData:entitiesData options:NSJSONReadingMutableContainers error:nil]; + NSDictionary *credentials = [[entities objectForKey:@"general"] objectForKey:@"credentials"]; self.serverUrl = [credentials objectForKey:@"url"]; self.token = [credentials objectForKey:@"token"]; } @@ -46,9 +45,9 @@ MattermostBucket *mattermostBucket; NSLog(@"ERROR %@", [error userInfo]); [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; + NSLog(@"invalidating session %@", self.requestId); + [session finishTasksAndInvalidate]; } - NSLog(@"invalidating session %@", self.requestId); - [session finishTasksAndInvalidate]; } -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { @@ -123,8 +122,6 @@ MattermostBucket *mattermostBucket; NSString* postAsString = [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding]; NSURL *createUrl = [NSURL URLWithString:[self.serverUrl stringByAppendingString:@"/api/v4/posts"]]; - NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[self.requestId stringByAppendingString:@"-post"]]; - config.sharedContainerIdentifier = self.appGroupId; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:createUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0]; [request setHTTPMethod:@"POST"]; @@ -132,6 +129,8 @@ MattermostBucket *mattermostBucket; [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; + + NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; NSLog(@"Executing post request"); diff --git a/ios/MattermostShare/ShareViewController.m b/ios/MattermostShare/ShareViewController.m index b75c9d763..c3699f174 100644 --- a/ios/MattermostShare/ShareViewController.m +++ b/ios/MattermostShare/ShareViewController.m @@ -1,7 +1,6 @@ #import "ShareViewController.h" #import #import -#import "MattermostBucket.h" #import "PerformRequests.h" NSExtensionContext* extensionContext; diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist index 2221397f4..f188ad74e 100644 --- a/ios/MattermostTests/Info.plist +++ b/ios/MattermostTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 82 + 84 diff --git a/share_extension/ios/extension_channels.js b/share_extension/ios/extension_channels.js index fcd1e911c..a8596bb09 100644 --- a/share_extension/ios/extension_channels.js +++ b/share_extension/ios/extension_channels.js @@ -12,10 +12,8 @@ import { import DeviceInfo from 'react-native-device-info'; import {intlShape} from 'react-intl'; -import {Client4} from 'mattermost-redux/client'; -import {General, Preferences} from 'mattermost-redux/constants'; -import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {General} from 'mattermost-redux/constants'; +import {getChannelsInTeam, getDirectChannels} from 'mattermost-redux/selectors/entities/channels'; import SearchBar from 'app/components/search_bar'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -26,8 +24,8 @@ import ExtensionNavBar from './extension_nav_bar'; export default class ExtensionChannels extends PureComponent { static propTypes = { + entities: PropTypes.object, currentChannelId: PropTypes.string.isRequired, - currentUserId: PropTypes.string.isRequired, navigator: PropTypes.object.isRequired, onSelectChannel: PropTypes.func.isRequired, teamId: PropTypes.string.isRequired, @@ -98,16 +96,6 @@ export default class ExtensionChannels extends PureComponent { this.setState({sections}); }; - getGroupDisplayNameFromUserIds = (userIds, profiles) => { - const names = []; - userIds.forEach((id) => { - const profile = profiles.find((p) => p.id === id); - names.push(displayUsername(profile, Preferences.DISPLAY_PREFER_FULL_NAME)); - }); - - return names.sort(this.sort).join(', '); - }; - goBack = () => { this.props.navigator.pop(); }; @@ -116,64 +104,13 @@ export default class ExtensionChannels extends PureComponent { loadChannels = async () => { try { - const {currentUserId, teamId} = this.props; - const channelsMap = {}; + const {entities, teamId} = this.props; // get the channels for the specified team - const myChannels = await Client4.getMyChannels(teamId); - - // filter channels that are direct and group messages - const dms = myChannels.filter((channel) => { - return channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL; - }); - - 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 && 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) { - 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 - } - } - } - - const channels = myChannels.map((channel) => { - return { - id: channel.id, - display_name: channelsMap[channel.id] || channel.display_name, - type: channel.type - }; - }); + const channelsInTeam = getChannelsInTeam({entities}); + const channelIds = channelsInTeam[teamId] || []; + const direct = getDirectChannels({entities}); + const channels = channelIds.map((id) => this.props.entities.channels.channels[id]).concat(direct); this.setState({ channels diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js index 1ff4db49d..817c1f6bb 100644 --- a/share_extension/ios/extension_post.js +++ b/share_extension/ios/extension_post.js @@ -40,6 +40,7 @@ import { import ExtensionChannels from './extension_channels'; import ExtensionNavBar from './extension_nav_bar'; import ExtensionTeams from './extension_teams'; +import {General} from 'mattermost-redux/constants/index'; const ShareExtension = NativeModules.MattermostShare; const MAX_INPUT_HEIGHT = 95; @@ -58,7 +59,8 @@ const extensionSvg = { export default class ExtensionPost extends PureComponent { static propTypes = { - credentials: PropTypes.object, + authenticated: PropTypes.bool.isRequired, + entities: PropTypes.object, navigator: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, theme: PropTypes.object.isRequired @@ -75,7 +77,7 @@ export default class ExtensionPost extends PureComponent { const isLandscape = width > height; this.state = { - currentUserId: null, + entities: props.entities, error: null, files: [], isLandscape, @@ -97,9 +99,9 @@ export default class ExtensionPost extends PureComponent { this.focusInput(); } - auth = async () => { + emmAuthenticationIfNeeded = async () => { try { - const emmSecured = await mattermostBucket.get('emm', Config.AppGroupId); + const emmSecured = await mattermostBucket.getPreference('emm', Config.AppGroupId); if (emmSecured) { const {intl} = this.context; await LocalAuth.authenticate({ @@ -132,7 +134,7 @@ export default class ExtensionPost extends PureComponent { goToChannels = wrapWithPreventDoubleTap(() => { const {navigator, theme} = this.props; - const {channel, currentUserId, team} = this.state; + const {channel, entities, team} = this.state; navigator.push({ component: ExtensionChannels, @@ -142,7 +144,7 @@ export default class ExtensionPost extends PureComponent { }, passProps: { currentChannelId: channel.id, - currentUserId, + entities, onSelectChannel: this.selectChannel, teamId: team.id, theme, @@ -164,6 +166,7 @@ export default class ExtensionPost extends PureComponent { backgroundColor: theme.centerChannelBg }, passProps: { + entities: this.state.entities, currentTeamId: team.id, onSelectTeam: this.selectTeam, theme @@ -180,12 +183,13 @@ export default class ExtensionPost extends PureComponent { }; loadData = async () => { - const {credentials} = this.props; - if (credentials) { + const {entities} = this.state; + if (this.props.authenticated) { try { - const currentUserId = await mattermostBucket.get('currentUserId', Config.AppGroupId); - const channel = await mattermostBucket.get('selectedChannel', Config.AppGroupId); - const team = await mattermostBucket.get('selectedTeam', Config.AppGroupId); + const {credentials} = entities.general; + const {currentUserId} = entities.users; + const team = entities.teams.teams[entities.teams.currentTeamId]; + const channel = entities.channels.channels[entities.channels.currentChannelId]; const items = await ShareExtension.data(Config.AppGroupId); const text = []; const urls = []; @@ -231,7 +235,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, totalSize}); + this.setState({channel, files, team, value, totalSize}); } catch (error) { this.setState({error}); } @@ -253,7 +257,7 @@ export default class ExtensionPost extends PureComponent { renderBody = (styles) => { const {formatMessage} = this.context.intl; - const {credentials, theme} = this.props; + const {authenticated, theme} = this.props; const {error, sending, totalSize, value} = this.state; if (sending) { @@ -283,7 +287,7 @@ export default class ExtensionPost extends PureComponent { ); } - if (credentials && !error) { + if (authenticated && !error) { return ( { const {formatMessage} = this.context.intl; - const {credentials, theme} = this.props; + const {authenticated, theme} = this.props; const {channel, sending} = this.state; const channelName = channel ? channel.display_name : ''; @@ -337,7 +341,7 @@ export default class ExtensionPost extends PureComponent { return null; } - if (!credentials) { + if (!authenticated) { return null; } @@ -376,7 +380,6 @@ export default class ExtensionPost extends PureComponent { renderFiles = (styles) => { const {files} = this.state; - return files.map((file, index) => { let component; @@ -456,7 +459,7 @@ export default class ExtensionPost extends PureComponent { renderTeamButton = (styles) => { const {formatMessage} = this.context.intl; - const {credentials, theme} = this.props; + const {authenticated, theme} = this.props; const {sending, team} = this.state; const teamName = team ? team.display_name : ''; @@ -464,7 +467,7 @@ export default class ExtensionPost extends PureComponent { return null; } - if (!credentials) { + if (!authenticated) { return null; } @@ -503,21 +506,35 @@ export default class ExtensionPost extends PureComponent { selectTeam = (team, channel) => { this.setState({channel, team}); + + // Update the channels for the team + Client4.getMyChannels(team.id).then((channels) => { + const defaultChannel = channels.find((c) => c.name === General.DEFAULT_CHANNEL && c.team_id === team.id); + this.updateChannelsInEntities(channels); + if (!channel) { + this.setState({channel: defaultChannel}); + } + }).catch((error) => { + this.setState({error}); + }); }; sendMessage = wrapWithPreventDoubleTap(async () => { - const {credentials, onClose} = this.props; - const {channel, currentUserId, files, value} = this.state; + const {authenticated, onClose} = this.props; + const {channel, entities, files, value} = this.state; + const {currentUserId} = entities.users; // If no text and no files do nothing if (!value && !files.length) { return; } - if (currentUserId && credentials) { - await this.auth(); + if (currentUserId && authenticated) { + await this.emmAuthenticationIfNeeded(); try { + // Check to see if the use still belongs to the channel + await Client4.getMyChannelMember(channel.id); const post = { user_id: currentUserId, channel_id: channel.id, @@ -541,23 +558,56 @@ export default class ExtensionPost extends PureComponent { } }); + updateChannelsInEntities = (newChannels) => { + const {entities} = this.state; + const newEntities = { + ...entities, + channels: { + ...entities.channels, + channels: {...entities.channels.channels}, + channelsInTeam: {...entities.channels.channelsInTeam} + } + }; + const {channels, channelsInTeam} = newEntities.channels; + + newChannels.forEach((c) => { + channels[c.id] = c; + const channelIdsInTeam = channelsInTeam[c.team_id]; + if (channelIdsInTeam) { + if (!channelIdsInTeam.includes(c.id)) { + channelsInTeam[c.team_id].push(c.id); + } + } else { + channelsInTeam[c.team_id] = [c.id]; + } + }); + + this.setState({entities: newEntities}); + mattermostBucket.writeToFile('entities', JSON.stringify(newEntities), Config.AppGroupId); + }; + render() { - const {credentials, theme} = this.props; - const {totalSize, sending} = this.state; + const {authenticated, theme} = this.props; + const {channel, totalSize, sending} = this.state; const {formatMessage} = this.context.intl; const styles = getStyleSheet(theme); + let postButtonText = formatMessage({id: 'mobile.share_extension.send', defaultMessage: 'Send'}); + if (totalSize >= MAX_FILE_SIZE || sending || !channel) { + postButtonText = null; + } + return ( = MAX_FILE_SIZE || sending) ? null : formatMessage({id: 'mobile.share_extension.send', defaultMessage: 'Send'})} + rightButtonTitle={postButtonText} theme={theme} /> {this.renderBody(styles)} diff --git a/share_extension/ios/extension_teams.js b/share_extension/ios/extension_teams.js index 0a1479d4b..485b46740 100644 --- a/share_extension/ios/extension_teams.js +++ b/share_extension/ios/extension_teams.js @@ -7,8 +7,8 @@ import {ActivityIndicator, FlatList, Text, View} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import {intlShape} from 'react-intl'; -import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; +import {getChannelsInTeam} from 'mattermost-redux/selectors/entities/channels'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -18,6 +18,7 @@ import ExtensionTeamItem from './extension_team_item'; export default class ExtensionTeams extends PureComponent { static propTypes = { currentTeamId: PropTypes.string.isRequired, + entities: PropTypes.object, navigator: PropTypes.object.isRequired, onSelectTeam: PropTypes.func.isRequired, theme: PropTypes.object.isRequired @@ -53,17 +54,24 @@ export default class ExtensionTeams extends PureComponent { loadTeams = async () => { try { const defaultChannels = {}; - const teams = await Client4.getMyTeams(); - const myMembers = await Client4.getMyTeamMembers(); + const {teams, myMembers} = this.props.entities.teams; const myTeams = []; + const channelsInTeam = getChannelsInTeam({entities: this.props.entities}); - 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); + for (const key in teams) { + if (teams.hasOwnProperty(key)) { + const team = teams[key]; + const belong = myMembers[key]; + if (belong) { + const channelIds = channelsInTeam[key]; + let channels; + if (channelIds) { + channels = channelIds.map((id) => this.props.entities.channels.channels[id]); + defaultChannels[team.id] = channels.find((channel) => channel.name === General.DEFAULT_CHANNEL); + } + + myTeams.push(team); + } } } diff --git a/share_extension/ios/index.js b/share_extension/ios/index.js index 86105796b..86d0081ae 100644 --- a/share_extension/ios/index.js +++ b/share_extension/ios/index.js @@ -37,8 +37,8 @@ export default class SharedApp extends PureComponent { isLandscape }; - mattermostBucket.get('credentials', Config.AppGroupId).then((value) => { - this.credentials = value; + mattermostBucket.readFromFile('entities', Config.AppGroupId).then((value) => { + this.entities = value; this.setState({init: true}); }); } @@ -72,6 +72,11 @@ export default class SharedApp extends PureComponent { } }; + userIsLoggedIn = () => { + return Boolean(this.entities && this.entities.general && this.entities.general.credentials && + this.entities.general.credentials.token && this.entities.general.credentials.url); + }; + render() { const {init, isLandscape} = this.state; @@ -86,7 +91,8 @@ export default class SharedApp extends PureComponent { component: ExtensionPost, title: 'Mattermost', passProps: { - credentials: this.credentials, + authenticated: this.userIsLoggedIn(), + entities: this.entities, onClose: this.onClose, isLandscape, theme @@ -113,7 +119,7 @@ export default class SharedApp extends PureComponent { styles.container, { opacity: this.state.containerOpacity, - height: this.credentials ? 250 : 130, + height: this.userIsLoggedIn() ? 250 : 130, top: isLandscape ? 20 : 65 } ]}