diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index 34989f131d..b159a2884c 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -35,7 +35,7 @@ export function handleTeamChange(teamId, selectChannel = true) { markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState); } - dispatch(batchActions(actions), getState); + dispatch(batchActions(actions, 'BATCH_SELECT_TEAM'), getState); }; } diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index b02f4d3c46..e6ef0c16b5 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -48,6 +48,7 @@ export default class SafeAreaIos extends PureComponent { } componentWillMount() { + this.mounted = true; this.getSafeAreaInsets(); this.mounted = true; } @@ -60,6 +61,7 @@ export default class SafeAreaIos extends PureComponent { } componentWillUnmount() { + this.mounted = false; Orientation.removeOrientationListener(this.getSafeAreaInsets); this.keyboardDidShowListener.remove(); this.keyboardDidHideListener.remove(); @@ -86,7 +88,9 @@ export default class SafeAreaIos extends PureComponent { if (this.isX) { SafeArea.getSafeAreaInsetsForRootView().then((result) => { const {safeAreaInsets} = result; - this.setState({safeAreaInsets}); + if (this.mounted) { + this.setState({safeAreaInsets}); + } }); } }; diff --git a/app/mattermost.js b/app/mattermost.js index 822e355d6b..48b9c3ddaf 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -49,6 +49,7 @@ import initialState from 'app/initial_state'; import PushNotifications from 'app/push_notifications'; import {registerScreens} from 'app/screens'; import configureStore from 'app/store'; +import mattermostBucket from 'app/mattermost_bucket'; import mattermostManaged from 'app/mattermost_managed'; import {deleteFileCache} from 'app/utils/file'; import {init as initAnalytics} from 'app/utils/segment'; @@ -210,6 +211,7 @@ export default class Mattermost { const intl = this.getIntl(); if (isSecured) { try { + mattermostBucket.set('emm', vendor, Config.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 new file mode 100644 index 0000000000..a858cc00bf --- /dev/null +++ b/app/mattermost_bucket/index.js @@ -0,0 +1,34 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {NativeModules, Platform} from 'react-native'; + +// TODO: Remove platform specific once android is implemented +const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucket : null; + +export default { + set: (key, value, groupName) => { + if (MattermostBucket) { + MattermostBucket.set(key, value, groupName); + } + }, + get: async (key, groupName) => { + if (MattermostBucket) { + const value = await MattermostBucket.get(key, groupName); + if (value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } + } + } + + return null; + }, + remove: (key, groupName) => { + if (MattermostBucket) { + MattermostBucket.remove(key, groupName); + } + } +}; diff --git a/app/store/index.js b/app/store/index.js index 753a27b15a..000864a68c 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -17,7 +17,7 @@ import networkConnectionListener from 'app/utils/network'; import {createSentryMiddleware} from 'app/utils/sentry/middleware'; import {promiseTimeout} from 'app/utils/promise_timeout'; -import {messageRetention} from './middleware'; +import {messageRetention, shareExtensionData} from './middleware'; import {transformSet} from './utils'; function getAppReducer() { @@ -211,7 +211,7 @@ export default function configureAppStore(initialState) { } }; - const additionalMiddleware = [createSentryMiddleware(), messageRetention]; + const additionalMiddleware = [createSentryMiddleware(), messageRetention, shareExtensionData]; return configureStore(initialState, appReducer, offlineOptions, getAppReducer, { additionalMiddleware }); diff --git a/app/store/middleware.js b/app/store/middleware.js index 5b135821c4..c67ca96262 100644 --- a/app/store/middleware.js +++ b/app/store/middleware.js @@ -3,8 +3,16 @@ 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 {ViewTypes} from 'app/constants'; import initialState from 'app/initial_state'; +import mattermostBucket from 'app/mattermost_bucket'; +import Config from 'assets/config'; import { captureException, @@ -305,3 +313,95 @@ function cleanupState(action, keepCurrent = false) { error: action.error }; } + +export function shareExtensionData(store) { + 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); + break; + } + return nextAction; + }; +} diff --git a/assets/base/config.json b/assets/base/config.json index 77d4d50964..d1ca7f1ff8 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -37,5 +37,7 @@ "EnableMobileClientUpgradeUserSetting": true, "EnableForceMobileClientUpgrade": true, "MobileClientUpgradeAndroidApkLink": "https://about.mattermost.com/mattermost-android-app/", - "MobileClientUpgradeIosIpaLink": "https://about.mattermost.com/mattermost-ios-app/" + "MobileClientUpgradeIosIpaLink": "https://about.mattermost.com/mattermost-ios-app/", + + "AppGroupId": "group.com.mattermost.rnbeta" } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 8a74931832..e08b4e7375 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2124,6 +2124,10 @@ "mobile.search.in_modifier_title": "channel-name", "mobile.search.jump": "JUMP", "mobile.search.no_results": "No Results Found", + "mobile.share_extension.cancel": "Cancel", + "mobile.share_extension.channel": "Channel", + "mobile.share_extension.send": "Send", + "mobile.share_extension.team": "Team", "mobile.select_team.choose": "Your teams:", "mobile.select_team.join_open": "Open teams you can join", "mobile.select_team.no_teams": "There are no available teams for you to join.", diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 1b09e04862..a4e9fcaa22 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -44,6 +44,10 @@ 7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */ = {isa = PBXBuildFile; fileRef = 7F292A701E8AB73400A450A3 /* SplashScreenResource */; }; 7F292AA61E8ABB1100A450A3 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */; }; 7F292AA71E8ABB1100A450A3 /* splash.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA51E8ABB1100A450A3 /* splash.png */; }; + 7F380D0B1FDB3CFC0061AAD2 /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F93F9D91FBB726B0088E416 /* libRCTVideo.a */; }; + 7F3F66021FE426EE0085CA0E /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */; }; + 7F3F660F1FE4280D0085CA0E /* libReactNativeExceptionHandler.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA7950B1F61A1A500C02924 /* libReactNativeExceptionHandler.a */; }; + 7F3F66101FE4281A0085CA0E /* libRNSentry.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA795061F61A1A500C02924 /* libRNSentry.a */; }; 7F43D5A01F6BF882001FC614 /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DD11281E79EBE1004111BA /* libRNDeviceInfo.a */; }; 7F43D5D61F6BF8C2001FC614 /* libRNLocalAuth.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */; }; 7F43D5D71F6BF8D0001FC614 /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */; }; @@ -59,12 +63,37 @@ 7F43D63F1F6BFA19001FC614 /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */; }; 7F43D6401F6BFA82001FC614 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */; }; 7F6877B31E7836070094B63F /* libToolTipMenu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6877B01E7835E50094B63F /* libToolTipMenu.a */; }; + 7F6C47A51FE87E8C00F5A912 /* PerformRequests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */; }; + 7FB6006B1FE3116800DB284F /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 375218411F4B9E320035444B /* libRNFetchBlob.a */; }; 7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; 7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */; }; + 7FC649EE1FE983660074E4C7 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 349FBA7338E74D9BBD709528 /* EvilIcons.ttf */; }; + 7FC649F01FE9B5D90074E4C7 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; }; 7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */; }; 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB10971F6101710039A015 /* BlurAppScreen.m */; }; 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */; }; + 7FF7BE2C1FDEE4AE005E55FE /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; + 7FF7BE6D1FDEE5E8005E55FE /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */; }; + 7FF7BE6E1FDEE5E8005E55FE /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */; }; + 7FF7BE6F1FDF3CE4005E55FE /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; + 7FF7BE701FDF3EE7005E55FE /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */; }; + 7FF7BE711FE004A3005E55FE /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DD11281E79EBE1004111BA /* libRNDeviceInfo.a */; }; + 7FF7BE721FE01FC7005E55FE /* libRCTOrientation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 374634671E848085005E1244 /* libRCTOrientation.a */; }; + 7FFDB3191FE3566C009E3BCF /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; }; + 7FFE329E1FD9CB650038C7A0 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */; }; + 7FFE32A11FD9CB650038C7A0 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */; }; + 7FFE32A51FD9CB650038C7A0 /* MattermostShare.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7FFE32E71FD9CCD00038C7A0 /* libART.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37ABD39C1F4CE13B001FDE6B /* libART.a */; }; + 7FFE32E81FD9CCDE0038C7A0 /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3752184F1F4B9E980035444B /* libRCTCameraRoll.a */; }; + 7FFE32E91FD9CCF40038C7A0 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; + 7FFE32EA1FD9CD050038C7A0 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 7FFE32EB1FD9CD170038C7A0 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; + 7FFE32EC1FD9CD360038C7A0 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + 7FFE32ED1FD9CD450038C7A0 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; + 7FFE32EE1FD9CD800038C7A0 /* libRNLocalAuth.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */; }; + 7FFE32EF1FD9CD800038C7A0 /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */; }; + 7FFE32F11FD9D64E0038C7A0 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */; }; 8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9263CF9B16054263B13EA23B /* libRNSafeArea.a */; }; @@ -431,20 +460,6 @@ remoteGlobalIDString = 4681C02C1B05271A004D67D4; remoteInfo = ToolTipMenuTests; }; - 7F6CEE231FDAEA0D0010135A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RNReactNativeDocViewer; - }; - 7F6CEE281FDAEA0D0010135A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 6DA7B8031F692C4C00FD1D50; - remoteInfo = RNSafeArea; - }; 7F8AAB3B1F4E0FEB00F5A52C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 50ECB1D221E44F51B5690DF2 /* FastImage.xcodeproj */; @@ -529,6 +544,27 @@ remoteGlobalIDString = 5DBEB1501B18CEA900B34395; remoteInfo = RNVectorIcons; }; + 7FFE328F1FD9CAF40038C7A0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RNReactNativeDocViewer; + }; + 7FFE32941FD9CAF40038C7A0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 6DA7B8031F692C4C00FD1D50; + remoteInfo = RNSafeArea; + }; + 7FFE32A31FD9CB650038C7A0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7FFE32991FD9CB640038C7A0; + remoteInfo = MattermostShare; + }; 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -549,6 +585,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + 7FFE32A91FD9CB650038C7A0 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 7FFE32A51FD9CB650038C7A0 /* MattermostShare.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -613,11 +660,14 @@ 7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = ""; }; 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = LaunchScreen.xib; path = SplashScreenResource/LaunchScreen.xib; sourceTree = ""; }; 7F292AA51E8ABB1100A450A3 /* splash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = splash.png; path = SplashScreenResource/splash.png; sourceTree = ""; }; + 7F380D0A1FDB28160061AAD2 /* MattermostShare.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MattermostShare.entitlements; sourceTree = ""; }; 7F43D5DF1F6BF994001FC614 /* libRNSVG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libRNSVG.a; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libRNSVG.a"; sourceTree = ""; }; 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; }; 7F6877AA1E7835E50094B63F /* ToolTipMenu.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ToolTipMenu.xcodeproj; path = "../node_modules/react-native-tooltip/ToolTipMenu.xcodeproj"; sourceTree = ""; }; + 7F6C47A31FE87E8C00F5A912 /* PerformRequests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PerformRequests.h; sourceTree = ""; }; + 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PerformRequests.m; sourceTree = ""; }; 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; }; 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNImagePicker.xcodeproj; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = ""; }; 7FEB10961F6101710039A015 /* BlurAppScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BlurAppScreen.h; path = Mattermost/BlurAppScreen.h; sourceTree = ""; }; @@ -626,6 +676,24 @@ 7FEB109A1F61019C0039A015 /* MattermostManaged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MattermostManaged.m; path = Mattermost/MattermostManaged.m; sourceTree = ""; }; 7FEB109B1F61019C0039A015 /* UIImage+ImageEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImage+ImageEffects.h"; path = "Mattermost/UIImage+ImageEffects.h"; sourceTree = ""; }; 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ImageEffects.m"; path = "Mattermost/UIImage+ImageEffects.m"; sourceTree = ""; }; + 7FF7BE6B1FDEE5E8005E55FE /* MattermostBucket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MattermostBucket.h; sourceTree = ""; }; + 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = ""; }; + 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MattermostShare.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE329C1FD9CB650038C7A0 /* ShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = ""; }; + 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = ""; }; + 7FFE32A01FD9CB650038C7A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 7FFE32A21FD9CB650038C7A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32B71FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32B81FD9CCAA0038C7A0 /* libKSCrashLib.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libKSCrashLib.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32B91FD9CCAA0038C7A0 /* libSDWebImage iOS static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libSDWebImage iOS static.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32BA1FD9CCAA0038C7A0 /* libSentryStatic.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libSentryStatic.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32BB1FD9CCAA0038C7A0 /* libXCDYouTubeKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libXCDYouTubeKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32BC1FD9CCAA0038C7A0 /* PerformanceBezier.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PerformanceBezier.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32BD1FD9CCAA0038C7A0 /* QuartzBookPack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = QuartzBookPack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32BE1FD9CCAA0038C7A0 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sentry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSafeArea.xcodeproj; path = "../node_modules/react-native-safe-area/ios/RNSafeArea.xcodeproj"; sourceTree = ""; }; 8606EB1EB7E349EF8248933E /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; @@ -706,6 +774,31 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7FFE32971FD9CB640038C7A0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FFE32E71FD9CCD00038C7A0 /* libART.a in Frameworks */, + 7F3F66101FE4281A0085CA0E /* libRNSentry.a in Frameworks */, + 7F3F660F1FE4280D0085CA0E /* libReactNativeExceptionHandler.a in Frameworks */, + 7FB6006B1FE3116800DB284F /* libRNFetchBlob.a in Frameworks */, + 7FF7BE721FE01FC7005E55FE /* libRCTOrientation.a in Frameworks */, + 7FF7BE711FE004A3005E55FE /* libRNDeviceInfo.a in Frameworks */, + 7FF7BE6F1FDF3CE4005E55FE /* libRNVectorIcons.a in Frameworks */, + 7F380D0B1FDB3CFC0061AAD2 /* libRCTVideo.a in Frameworks */, + 7FFE32F11FD9D64E0038C7A0 /* libRCTWebSocket.a in Frameworks */, + 7FFE32EE1FD9CD800038C7A0 /* libRNLocalAuth.a in Frameworks */, + 7FFE32EF1FD9CD800038C7A0 /* libRNPasscodeStatus.a in Frameworks */, + 7F3F66021FE426EE0085CA0E /* libRNSVG.a in Frameworks */, + 7FFE32ED1FD9CD450038C7A0 /* libRCTNetwork.a in Frameworks */, + 7FFE32EC1FD9CD360038C7A0 /* libRCTText.a in Frameworks */, + 7FFE32EB1FD9CD170038C7A0 /* libRCTImage.a in Frameworks */, + 7FFE32EA1FD9CD050038C7A0 /* libReact.a in Frameworks */, + 7FFE32E91FD9CCF40038C7A0 /* libRCTAnimation.a in Frameworks */, + 7FFE32E81FD9CCDE0038C7A0 /* libRCTCameraRoll.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -828,6 +921,8 @@ 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */, + 7FF7BE6B1FDEE5E8005E55FE /* MattermostBucket.h */, + 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */, 7FEB10991F61019C0039A015 /* MattermostManaged.h */, 7FEB109A1F61019C0039A015 /* MattermostManaged.m */, 7F292AA51E8ABB1100A450A3 /* splash.png */, @@ -941,6 +1036,17 @@ 4B992D7BAAEDF8759DB525B5 /* Frameworks */ = { isa = PBXGroup; children = ( + 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */, + 7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */, + 7FFE32B71FD9CCAA0038C7A0 /* KSCrash.framework */, + 7FFE32B81FD9CCAA0038C7A0 /* libKSCrashLib.a */, + 7FFE32B91FD9CCAA0038C7A0 /* libSDWebImage iOS static.a */, + 7FFE32BA1FD9CCAA0038C7A0 /* libSentryStatic.a */, + 7FFE32BB1FD9CCAA0038C7A0 /* libXCDYouTubeKit.a */, + 7FFE32BC1FD9CCAA0038C7A0 /* PerformanceBezier.framework */, + 7FFE32BD1FD9CCAA0038C7A0 /* QuartzBookPack.framework */, + 7FFE32BE1FD9CCAA0038C7A0 /* SDWebImage.framework */, + 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */, 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */, 7F43D5DF1F6BF994001FC614 /* libRNSVG.a */, 65FD5EA57EBAE06106094B2F /* libPods-Mattermost.a */, @@ -1012,22 +1118,6 @@ name = Products; sourceTree = ""; }; - 7F6CEE201FDAEA0D0010135A /* Products */ = { - isa = PBXGroup; - children = ( - 7F6CEE241FDAEA0D0010135A /* libRNReactNativeDocViewer.a */, - ); - name = Products; - sourceTree = ""; - }; - 7F6CEE251FDAEA0D0010135A /* Products */ = { - isa = PBXGroup; - children = ( - 7F6CEE291FDAEA0D0010135A /* libRNSafeArea.a */, - ); - name = Products; - sourceTree = ""; - }; 7F8AAB371F4E0FEB00F5A52C /* Products */ = { isa = PBXGroup; children = ( @@ -1118,6 +1208,36 @@ name = Products; sourceTree = ""; }; + 7FFE328C1FD9CAF40038C7A0 /* Products */ = { + isa = PBXGroup; + children = ( + 7FFE32901FD9CAF40038C7A0 /* libRNReactNativeDocViewer.a */, + ); + name = Products; + sourceTree = ""; + }; + 7FFE32911FD9CAF40038C7A0 /* Products */ = { + isa = PBXGroup; + children = ( + 7FFE32951FD9CAF40038C7A0 /* libRNSafeArea.a */, + ); + name = Products; + sourceTree = ""; + }; + 7FFE329B1FD9CB650038C7A0 /* MattermostShare */ = { + isa = PBXGroup; + children = ( + 7F380D0A1FDB28160061AAD2 /* MattermostShare.entitlements */, + 7FFE329C1FD9CB650038C7A0 /* ShareViewController.h */, + 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */, + 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */, + 7FFE32A21FD9CB650038C7A0 /* Info.plist */, + 7F6C47A31FE87E8C00F5A912 /* PerformRequests.h */, + 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */, + ); + path = MattermostShare; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( @@ -1176,6 +1296,7 @@ 13B07FAE1A68108700A75B9A /* Mattermost */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* MattermostTests */, + 7FFE329B1FD9CB650038C7A0 /* MattermostShare */, 83CBBA001A601CBA00E9B192 /* Products */, 0156F464626F49C2977D7982 /* Resources */, 37DF8AC51F5F0D410079BF89 /* Recovered References */, @@ -1191,6 +1312,7 @@ children = ( 13B07F961A680F5B00A75B9A /* Mattermost.app */, 00E356EE1AD99517003FC87E /* MattermostTests.xctest */, + 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */, ); name = Products; sourceTree = ""; @@ -1232,16 +1354,36 @@ AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */, BC5024E82B42A23CE05E72ED /* [CP] Embed Pods Frameworks */, 509B02AF14835503C5C6029D /* [CP] Copy Pods Resources */, + 7FFE32A91FD9CB650038C7A0 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( + 7FFE32A41FD9CB650038C7A0 /* PBXTargetDependency */, ); name = Mattermost; productName = "Hello World"; productReference = 13B07F961A680F5B00A75B9A /* Mattermost.app */; productType = "com.apple.product-type.application"; }; + 7FFE32991FD9CB640038C7A0 /* MattermostShare */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7FFE32A61FD9CB650038C7A0 /* Build configuration list for PBXNativeTarget "MattermostShare" */; + buildPhases = ( + 7FFE32961FD9CB640038C7A0 /* Sources */, + 7FFE32971FD9CB640038C7A0 /* Frameworks */, + 7FFE32981FD9CB640038C7A0 /* Resources */, + 7FFE32F01FD9D1F00038C7A0 /* Bundle React Native code and images */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MattermostShare; + productName = MattermostShare; + productReference = 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1259,11 +1401,28 @@ DevelopmentTeam = UQ8HT4Q2XM; ProvisioningStyle = Automatic; SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + com.apple.BackgroundModes = { + enabled = 1; + }; com.apple.Push = { enabled = 1; }; }; }; + 7FFE32991FD9CB640038C7A0 = { + CreatedOnToolsVersion = 9.1; + DevelopmentTeam = UQ8HT4Q2XM; + LastSwiftMigration = 0920; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Mattermost" */; @@ -1395,11 +1554,11 @@ ProjectRef = EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */; }, { - ProductGroup = 7F6CEE201FDAEA0D0010135A /* Products */; + ProductGroup = 7FFE328C1FD9CAF40038C7A0 /* Products */; ProjectRef = 41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */; }, { - ProductGroup = 7F6CEE251FDAEA0D0010135A /* Products */; + ProductGroup = 7FFE32911FD9CAF40038C7A0 /* Products */; ProjectRef = 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */; }, { @@ -1423,6 +1582,7 @@ targets = ( 13B07F861A680F5B00A75B9A /* Mattermost */, 00E356ED1AD99517003FC87E /* MattermostTests */, + 7FFE32991FD9CB640038C7A0 /* MattermostShare */, ); }; /* End PBXProject section */ @@ -1771,20 +1931,6 @@ remoteRef = 7F6877B11E7835E50094B63F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 7F6CEE241FDAEA0D0010135A /* libRNReactNativeDocViewer.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNReactNativeDocViewer.a; - remoteRef = 7F6CEE231FDAEA0D0010135A /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 7F6CEE291FDAEA0D0010135A /* libRNSafeArea.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNSafeArea.a; - remoteRef = 7F6CEE281FDAEA0D0010135A /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 7F8AAB3C1F4E0FEB00F5A52C /* libFastImage.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1869,6 +2015,20 @@ remoteRef = 7FDF290B1E1F4B4E00DBBE56 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FFE32901FD9CAF40038C7A0 /* libRNReactNativeDocViewer.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNReactNativeDocViewer.a; + remoteRef = 7FFE328F1FD9CAF40038C7A0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7FFE32951FD9CAF40038C7A0 /* libRNSafeArea.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNSafeArea.a; + remoteRef = 7FFE32941FD9CAF40038C7A0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1918,6 +2078,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7FFE32981FD9CB640038C7A0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FC649F01FE9B5D90074E4C7 /* OpenSans-Bold.ttf in Resources */, + 7FC649EE1FE983660074E4C7 /* EvilIcons.ttf in Resources */, + 7FF7BE701FDF3EE7005E55FE /* Ionicons.ttf in Resources */, + 7FFDB3191FE3566C009E3BCF /* FontAwesome.ttf in Resources */, + 7FFE32A11FD9CB650038C7A0 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1998,6 +2170,20 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Mattermost/Pods-Mattermost-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 7FFE32F01FD9D1F00038C7A0 /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = ./bundleReactNative.sh; + }; AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2063,11 +2249,23 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, + 7FF7BE6D1FDEE5E8005E55FE /* MattermostBucket.m in Sources */, 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */, 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 7FFE32961FD9CB640038C7A0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FF7BE6E1FDEE5E8005E55FE /* MattermostBucket.m in Sources */, + 7FF7BE2C1FDEE4AE005E55FE /* MattermostManaged.m in Sources */, + 7FFE329E1FD9CB650038C7A0 /* ShareViewController.m in Sources */, + 7F6C47A51FE87E8C00F5A912 /* PerformRequests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2076,8 +2274,24 @@ target = 13B07F861A680F5B00A75B9A /* Mattermost */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; + 7FFE32A41FD9CB650038C7A0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7FFE32991FD9CB640038C7A0 /* MattermostShare */; + targetProxy = 7FFE32A31FD9CB650038C7A0 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7FFE32A01FD9CB650038C7A0 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; @@ -2152,9 +2366,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 634A8F047C73D24A87850EC0 /* Pods-Mattermost.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 78; DEAD_CODE_STRIPPING = NO; @@ -2163,7 +2377,6 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-smart-splash-screen/ios/RCTSplashScreen/RCTSplashScreen/**", "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", "$(SRCROOT)/../node_modules/react-native-local-auth", @@ -2202,9 +2415,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = BFB7025EA936C1B5DC9725C2 /* Pods-Mattermost.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 78; DEAD_CODE_STRIPPING = NO; @@ -2213,7 +2426,6 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", - "$(SRCROOT)/../node_modules/react-native-smart-splash-screen/ios/RCTSplashScreen/RCTSplashScreen/**", "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", "$(SRCROOT)/../node_modules/react-native-local-auth", @@ -2248,6 +2460,122 @@ }; name = Release; }; + 7FFE32A71FD9CB650038C7A0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = MattermostShare/MattermostShare.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = UQ8HT4Q2XM; + GCC_C_LANGUAGE_STANDARD = gnu11; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", + "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", + "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", + "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", + "$(SRCROOT)/../node_modules/react-native-local-auth", + "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", + "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey", + "$(SRCROOT)/../node_modules/react-native/Libraries/**", + "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**", + "$(SRCROOT)/../node_modules/react-native-fetch-blob/ios/**", + "$(SRCROOT)/../node_modules/react-native-exception-handler/ios", + "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", + "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS", + "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-youtube/**", + "$(SRCROOT)/../node_modules/react-native-video/ios", + "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", + "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + ); + INFOPLIST_FILE = MattermostShare/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.MattermostShare; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "MattermostShare/MattermostShare-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7FFE32A81FD9CB650038C7A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = MattermostShare/MattermostShare.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = UQ8HT4Q2XM; + GCC_C_LANGUAGE_STANDARD = gnu11; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", + "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", + "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", + "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", + "$(SRCROOT)/../node_modules/react-native-local-auth", + "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", + "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey", + "$(SRCROOT)/../node_modules/react-native/Libraries/**", + "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**", + "$(SRCROOT)/../node_modules/react-native-fetch-blob/ios/**", + "$(SRCROOT)/../node_modules/react-native-exception-handler/ios", + "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", + "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS", + "$(SRCROOT)/../node_modules/react-native-image-picker/ios", + "$(SRCROOT)/../node_modules/react-native-youtube/**", + "$(SRCROOT)/../node_modules/react-native-video/ios", + "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", + "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + ); + INFOPLIST_FILE = MattermostShare/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.MattermostShare; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "MattermostShare/MattermostShare-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2353,6 +2681,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7FFE32A61FD9CB650038C7A0 /* Build configuration list for PBXNativeTarget "MattermostShare" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7FFE32A71FD9CB650038C7A0 /* Debug */, + 7FFE32A81FD9CB650038C7A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Mattermost" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 1f7db23b96..b173973480 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -36,6 +36,36 @@ return YES; } +-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(nonnull NSString *)identifier completionHandler:(nonnull void (^)(void))completionHandler { + + NSUserDefaults *bucket = [[NSUserDefaults alloc] initWithSuiteName: @"group.com.mattermost"]; + NSString *credentialsString = [bucket objectForKey:@"credentials"]; + NSData *credentialsData = [credentialsString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *credentials = [NSJSONSerialization JSONObjectWithData:credentialsData options:NSJSONReadingMutableContainers error:nil]; + NSString *server = [credentials objectForKey:@"url"]; + NSString *token = [credentials objectForKey:@"token"]; + + NSDictionary *post = [NSDictionary dictionaryWithObjectsAndKeys:@"user_id", [bucket objectForKey:@"currentUserId"], @"message", @"Shit fuck", @"channel_id", @"zw43c5ttrjyu9dg7jnudwuz6bw"]; + NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:nil]; + NSString* postAsString = [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding]; + + NSURL *createUrl = [NSURL URLWithString:[server stringByAppendingString:@"/api/v4/posts"]]; + NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"backgroundSession-post"]; + config.sharedContainerIdentifier = @"group.com.mattermost"; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:createUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0]; + [request setHTTPMethod:@"POST"]; + [request setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; + NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config]; + NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; + [createTask resume]; + + completionHandler(); +} + // Required for orientation - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { return [Orientation getOrientation]; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 1d80f80ae5..baf943464b 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -2,6 +2,8 @@ + BundleEntryFilename + index.js CFBundleDevelopmentRegion en CFBundleDisplayName @@ -9,7 +11,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) + com.mattermost.rnbeta CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -81,6 +83,10 @@ OpenSans-Semibold.ttf OpenSans-SemiboldItalic.ttf + UIBackgroundModes + + fetch + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/ios/Mattermost/Mattermost.entitlements b/ios/Mattermost/Mattermost.entitlements index 903def2af5..4c980b9c54 100644 --- a/ios/Mattermost/Mattermost.entitlements +++ b/ios/Mattermost/Mattermost.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.security.application-groups + + group.com.mattermost.rnbeta + diff --git a/ios/Mattermost/MattermostManaged.h b/ios/Mattermost/MattermostManaged.h index 5b40d22432..3300ffb4ad 100644 --- a/ios/Mattermost/MattermostManaged.h +++ b/ios/Mattermost/MattermostManaged.h @@ -10,7 +10,7 @@ @interface MattermostManaged : NSObject - +- (NSUserDefaults *)bucketByName:(NSString*)name; + (void)sendConfigChangedEvent; @end diff --git a/ios/MattermostBucket.h b/ios/MattermostBucket.h new file mode 100644 index 0000000000..1e43783f5b --- /dev/null +++ b/ios/MattermostBucket.h @@ -0,0 +1,6 @@ +#import +#import "React/RCTBridgeModule.h" + +@interface MattermostBucket : NSObject +- (NSUserDefaults *)bucketByName:(NSString*)name; +@end diff --git a/ios/MattermostBucket.m b/ios/MattermostBucket.m new file mode 100644 index 0000000000..8772850b01 --- /dev/null +++ b/ios/MattermostBucket.m @@ -0,0 +1,54 @@ +// +// MattermostBucket.m +// Mattermost +// +// Created by Elias Nahum on 12/11/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import "MattermostBucket.h" + +@implementation MattermostBucket + +- (NSUserDefaults *)bucketByName:(NSString*)name { + return [[NSUserDefaults alloc] initWithSuiteName: name]; +} + ++ (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(get:(NSString *) key + bucketName:(NSString*) bucketName + getWithResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSUserDefaults* bucket = [self bucketByName: bucketName]; + id value = [bucket objectForKey:key]; + + if (value == nil) { + value = [NSNull null]; + } + + resolve(value); +} + +RCT_EXPORT_METHOD(remove:(NSString *) key + bucketName:(NSString*) bucketName) +{ + NSUserDefaults* bucket = [self bucketByName: bucketName]; + [bucket removeObjectForKey: key]; +} + +@end diff --git a/ios/MattermostShare/Base.lproj/MainInterface.storyboard b/ios/MattermostShare/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000000..a141885030 --- /dev/null +++ b/ios/MattermostShare/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist new file mode 100644 index 0000000000..a3d1f356e9 --- /dev/null +++ b/ios/MattermostShare/Info.plist @@ -0,0 +1,65 @@ + + + + + BundleEntryFilename + share.ios.js + BundleForced + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Mattermost + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.mattermost.rnbeta.MattermostShare + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.5.1 + CFBundleVersion + 74 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsAttachmentsWithMaxCount + 5 + NSExtensionActivationSupportsFileWithMaxCount + 5 + NSExtensionActivationSupportsImageWithMaxCount + 5 + NSExtensionActivationSupportsMovieWithMaxCount + 5 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + UIAppFonts + + Ionicons.ttf + FontAwesome.ttf + EvilIcons.ttf + OpenSans-Bold.ttf + + + diff --git a/ios/MattermostShare/MattermostShare.entitlements b/ios/MattermostShare/MattermostShare.entitlements new file mode 100644 index 0000000000..55fdcdea61 --- /dev/null +++ b/ios/MattermostShare/MattermostShare.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.mattermost.rnbeta + + + diff --git a/ios/MattermostShare/PerformRequests.h b/ios/MattermostShare/PerformRequests.h new file mode 100644 index 0000000000..17edcfa3ff --- /dev/null +++ b/ios/MattermostShare/PerformRequests.h @@ -0,0 +1,28 @@ +// +// PerformRequests.h +// MattermostShare +// +// Created by Elias Nahum on 12/18/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +@interface PerformRequests : NSObject +@property (nonatomic, strong) NSString *appGroupId; +@property (nonatomic, strong) NSString *requestId; +@property (nonatomic, strong) NSMutableArray *fileIds; +@property (nonatomic, strong) NSArray *files; +@property (nonatomic, strong) NSDictionary *post; + +@property (nonatomic, strong) NSString *serverUrl; +@property (nonatomic, strong) NSString *token; +@property NSUserDefaults *bucket; + +- (id) initWithPost:(NSDictionary *) post + withFiles:(NSArray *) files + forRequestId:(NSString *)requestId + inAppGroupId:(NSString *) appGroupId; + +-(void)createPost; +@end diff --git a/ios/MattermostShare/PerformRequests.m b/ios/MattermostShare/PerformRequests.m new file mode 100644 index 0000000000..b3d9819b09 --- /dev/null +++ b/ios/MattermostShare/PerformRequests.m @@ -0,0 +1,156 @@ +// +// PerformRequests.m +// MattermostShare +// +// Created by Elias Nahum on 12/18/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import "PerformRequests.h" +#import "MattermostBucket.h" +#import "ShareViewController.h" + +@implementation PerformRequests +MattermostBucket *mattermostBucket; + +- (id) initWithPost:(NSDictionary *) post + withFiles:(NSArray *) files + forRequestId:(NSString *)requestId + inAppGroupId:(NSString *) appGroupId { + self = [super init]; + if (self) { + self.post = post; + self.files = files; + self.appGroupId = appGroupId; + self.requestId = requestId; + + mattermostBucket = [[MattermostBucket alloc] init]; + self.bucket = [mattermostBucket bucketByName: appGroupId]; + [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]; + self.serverUrl = [credentials objectForKey:@"url"]; + self.token = [credentials objectForKey:@"token"]; +} + +-(void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(nullable NSError *)error { + if(error != nil) { + NSLog(@"ERROR %@", [error userInfo]); + } + NSLog(@"invalidating session %@", self.requestId); + [session finishTasksAndInvalidate]; +} + +-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { + NSString *sessionRequestId = [[session configuration] identifier]; + + if ([sessionRequestId containsString:@"files"]) { + NSLog(@"Got file response"); + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (json != nil) { + NSArray *fileInfos = [json objectForKey:@"file_infos"]; + self.fileIds = [[NSMutableArray alloc] init]; + for (id file in fileInfos) { + [self.fileIds addObject:[file objectForKey:@"id"]]; + } + NSLog(@"Calling sendPostRequest"); + [self sendPostRequest]; + } + + NSLog(@"Cleaning temp files"); + [self cleanUpTempFiles]; + } +} + +-(void)createPost { + NSString *channelId = [self.post objectForKey:@"channel_id"]; + + NSURL *filesUrl = [NSURL URLWithString:[self.serverUrl stringByAppendingString:@"/api/v4/files"]]; + + if (self.files != nil && [self.files count] > 0) { + NSString *POST_BODY_BOUNDARY = @"mobile.client.file.upload"; + NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[self.requestId stringByAppendingString:@"-files"]]; + config.sharedContainerIdentifier = self.appGroupId; + + NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:filesUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:120.0]; + [uploadRequest setHTTPMethod:@"POST"]; + [uploadRequest setValue:[@"Bearer " stringByAppendingString:self.token] forHTTPHeaderField:@"Authorization"]; + [uploadRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + NSString *contentTypeValue = [NSString stringWithFormat:@"multipart/form-data;boundary=%@", POST_BODY_BOUNDARY]; + [uploadRequest addValue:contentTypeValue forHTTPHeaderField:@"Content-Type"]; + + NSMutableData *dataForm = [NSMutableData alloc]; + [dataForm appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", POST_BODY_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]]; + [dataForm appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"channel_id\";\r\n\r\n%@", channelId] dataUsingEncoding:NSUTF8StringEncoding]]; + + for (id file in self.files) { + NSData *fileData = [NSData dataWithContentsOfFile:[file objectForKey:@"filePath"]]; + NSString *mimeType = [file objectForKey:@"mimeType"]; + NSLog(@"MimeType %@", mimeType); + [dataForm appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", POST_BODY_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]]; + [dataForm appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"files\"; filename=\"%@\"\r\n", + [file objectForKey:@"filename"]] dataUsingEncoding:NSUTF8StringEncoding]]; + [dataForm appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimeType] dataUsingEncoding:NSUTF8StringEncoding]]; + [dataForm appendData:[NSData dataWithData:fileData]]; + } + + [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]; + NSURLSessionDataTask *uploadTask = [uploadSession dataTaskWithRequest:uploadRequest]; + NSLog(@"Executing file request"); + [uploadTask resume]; + } else { + [self sendPostRequest]; + } +} + +-(void)sendPostRequest { + NSMutableDictionary *post = [self.post mutableCopy]; + [post setValue:self.fileIds forKey:@"file_ids"]; + NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:nil]; + 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"]; + [request setValue:[@"Bearer " stringByAppendingString:self.token] forHTTPHeaderField:@"Authorization"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; + NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; + NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; + NSLog(@"Executing post request"); + [createTask resume]; +} + +- (void) cleanUpTempFiles { + NSURL *tmpDirectoryURL = [ShareViewController tempContainerURL:self.appGroupId]; + if (tmpDirectoryURL == nil) { + return; + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + NSArray *tmpFiles = [fileManager contentsOfDirectoryAtPath:[tmpDirectoryURL path] error:&error]; + if (error) { + return; + } + + for (NSString *file in tmpFiles) + { + error = nil; + [fileManager removeItemAtPath:[[tmpDirectoryURL URLByAppendingPathComponent:file] path] error:&error]; + } +} +@end diff --git a/ios/MattermostShare/ShareViewController.h b/ios/MattermostShare/ShareViewController.h new file mode 100644 index 0000000000..ade93ce226 --- /dev/null +++ b/ios/MattermostShare/ShareViewController.h @@ -0,0 +1,6 @@ +#import +#import "React/RCTBridgeModule.h" + +@interface ShareViewController : UIViewController ++ (NSURL*) tempContainerURL: (NSString*)appGroupId; +@end diff --git a/ios/MattermostShare/ShareViewController.m b/ios/MattermostShare/ShareViewController.m new file mode 100644 index 0000000000..ed2532f49b --- /dev/null +++ b/ios/MattermostShare/ShareViewController.m @@ -0,0 +1,274 @@ +#import "ShareViewController.h" +#import +#import +#import "MattermostBucket.h" +#import "PerformRequests.h" + +NSExtensionContext* extensionContext; + +@implementation ShareViewController ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +- (UIView*) shareView { + NSURL *jsCodeLocation; + + jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"share.ios" fallbackResource:nil]; + + RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation + moduleName:@"MattermostShare" + initialProperties:nil + launchOptions:nil]; + rootView.backgroundColor = nil; + return rootView; +} + +RCT_EXPORT_MODULE(MattermostShare); + +- (void)viewDidLoad { + [super viewDidLoad]; + extensionContext = self.extensionContext; + UIView *rootView = [self shareView]; + if (rootView.backgroundColor == nil) { + rootView.backgroundColor = [[UIColor alloc] initWithRed:1 green:1 blue:1 alpha:0.1]; + } + + self.view = rootView; +} + +RCT_REMAP_METHOD(getOrientation, + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + if([UIScreen mainScreen].bounds.size.width < [UIScreen mainScreen].bounds.size.height) { + resolve(@"PORTRAIT"); + } else { + resolve(@"LANDSCAPE"); + } +} + +RCT_EXPORT_METHOD(close:(NSDictionary *)data appGroupId:(NSString *)appGroupId) { + if (data != nil) { + NSDictionary *post = [data objectForKey:@"post"]; + 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]; + [request createPost]; + } + + [extensionContext completeRequestReturningItems:nil + completionHandler:nil]; + NSLog(@"Extension closed"); +} + +RCT_REMAP_METHOD(data, + appGroupId: (NSString *)appGroupId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self extractDataFromContext: extensionContext withAppGroup: appGroupId andCallback:^(NSArray* items ,NSError* err) { + if (err) { + reject(@"data", @"Failed to extract attachment content", err); + return; + } + resolve(items); + }]; +} + +typedef void (^ProviderCallback)(NSString *content, NSString *contentType, BOOL owner, NSError *err); + +- (void)extractDataFromContext:(NSExtensionContext *)context withAppGroup:(NSString *) appGroupId andCallback:(void(^)(NSArray *items ,NSError *err))callback { + @try { + NSExtensionItem *item = [context.inputItems firstObject]; + NSArray *attachments = item.attachments; + NSMutableArray *items = [[NSMutableArray alloc] init]; + + __block int attachmentIdx = 0; + __block ProviderCallback providerCb = nil; + __block __weak ProviderCallback weakProviderCb = nil; + providerCb = ^ void (NSString *content, NSString *contentType, BOOL owner, NSError *err) { + if (err) { + callback(nil, err); + return; + } + + if (content != nil) { + [items addObject:@{ + @"type": contentType, + @"value": content, + @"owner": [NSNumber numberWithBool:owner], + }]; + } + + ++attachmentIdx; + if (attachmentIdx == [attachments count]) { + callback(items, nil); + } else { + [self extractDataFromProvider:attachments[attachmentIdx] withAppGroup:appGroupId andCallback: weakProviderCb]; + } + }; + weakProviderCb = providerCb; + [self extractDataFromProvider:attachments[0] withAppGroup:appGroupId andCallback: providerCb]; + } + @catch (NSException *exc) { + NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:1 userInfo:@{ + @"reason": [exc description] + }]; + callback(nil, error); + } +} + +- (void)extractDataFromProvider:(NSItemProvider *)provider withAppGroup:(NSString *) appGroupId andCallback:(void(^)(NSString* content, NSString* contentType, BOOL owner, NSError *err))callback { + if([provider hasItemConformingToTypeIdentifier:@"public.movie"]) { + [provider loadItemForTypeIdentifier:@"public.movie" options:nil completionHandler:^(id item, NSError *error) { + @try { + if ([item isKindOfClass: NSURL.class]) { + NSURL *url = (NSURL *)item; + return callback([url absoluteString], @"public.movie", NO, nil); + } + return callback(nil, nil, NO, nil); + } + @catch(NSException *exc) { + NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:2 userInfo:@{ + @"reason": [exc description] + }]; + callback(nil, nil, NO, error); + } + }]; + return; + } + if([provider hasItemConformingToTypeIdentifier:@"public.image"]) { + [provider loadItemForTypeIdentifier:@"public.image" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + @try { + if ([item isKindOfClass: NSURL.class]) { + NSURL *url = (NSURL *)item; + return callback([url absoluteString], @"public.image", NO, nil); + } else if ([item isKindOfClass: UIImage.class]) { + UIImage *image = (UIImage *)item; + NSString *fileName = [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]; + NSURL *tempContainerURL = [ShareViewController tempContainerURL:appGroupId]; + if (tempContainerURL == nil){ + return callback(nil, nil, NO, nil); + } + + NSURL *tempFileURL = [tempContainerURL URLByAppendingPathComponent: fileName]; + BOOL created = [UIImageJPEGRepresentation(image, 1) writeToFile:[tempFileURL path] atomically:YES]; + if (created) { + return callback([tempFileURL absoluteString], @"public.image", YES, nil); + } else { + return callback(nil, nil, NO, nil); + } + } else if ([item isKindOfClass: NSData.class]) { + NSString *fileName = [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]; + NSData *data = (NSData *)item; + UIImage *image = [UIImage imageWithData:data]; + NSURL *tempContainerURL = [ShareViewController tempContainerURL:appGroupId]; + if (tempContainerURL == nil){ + return callback(nil, nil, NO, nil); + } + NSURL *tempFileURL = [tempContainerURL URLByAppendingPathComponent: fileName]; + BOOL created = [UIImageJPEGRepresentation(image, 0.95) writeToFile:[tempFileURL path] atomically:YES]; + if (created) { + return callback([tempFileURL absoluteString], @"public.image", YES, nil); + } else { + return callback(nil, nil, NO, nil); + } + } else { + // Do nothing, some type we don't support. + return callback(nil, nil, NO, nil); + } + } + @catch(NSException *exc) { + NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:2 userInfo:@{ + @"reason": [exc description] + }]; + callback(nil, nil, NO, error); + } + }]; + return; + } + + if([provider hasItemConformingToTypeIdentifier:@"public.file-url"]) { + [provider loadItemForTypeIdentifier:@"public.file-url" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + if ([item isKindOfClass:NSURL.class]) { + return callback([(NSURL *)item absoluteString], @"public.file-url", NO, nil); + } else if ([item isKindOfClass:NSString.class]) { + return callback((NSString *)item, @"public.file-url", NO, nil); + } + callback(nil, nil, NO, nil); + }]; + return; + } + + if([provider hasItemConformingToTypeIdentifier:@"public.url"]) { + [provider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + if ([item isKindOfClass:NSURL.class]) { + return callback([(NSURL *)item absoluteString], @"public.url", NO, nil); + } else if ([item isKindOfClass:NSString.class]) { + return callback((NSString *)item, @"public.url", NO, nil); + } + }]; + return; + } + + if([provider hasItemConformingToTypeIdentifier:@"public.plain-text"]) { + [provider loadItemForTypeIdentifier:@"public.plain-text" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + if ([item isKindOfClass:NSString.class]) { + return callback((NSString *)item, @"public.plain-text", NO, nil); + } else if ([item isKindOfClass:NSAttributedString.class]) { + NSAttributedString *str = (NSAttributedString *)item; + return callback([str string], @"public.plain-text", NO, nil); + } else if ([item isKindOfClass:NSData.class]) { + NSString *str = [[NSString alloc] initWithData:(NSData *)item encoding:NSUTF8StringEncoding]; + if (str) { + return callback(str, @"public.plain-text", NO, nil); + } else { + return callback(nil, nil, NO, nil); + } + } else { + return callback(nil, nil, NO, nil); + } + }]; + return; + } + + callback(nil, nil, NO, nil); +} + ++ (NSURL*) tempContainerURL: (NSString*)appGroupId { + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *containerURL = [manager containerURLForSecurityApplicationGroupIdentifier: appGroupId]; + NSURL *tempDirectoryURL = [containerURL URLByAppendingPathComponent:@"shareTempItems"]; + if (![manager fileExistsAtPath:[tempDirectoryURL path]]) { + NSError *err; + [manager createDirectoryAtURL:tempDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err]; + if (err) { + return nil; + } + } + + return tempDirectoryURL; +} +@end diff --git a/ios/bundleReactNative.sh b/ios/bundleReactNative.sh index d7cb8ca577..f9b2294328 100755 --- a/ios/bundleReactNative.sh +++ b/ios/bundleReactNative.sh @@ -8,8 +8,8 @@ if [[ "${SENTRY_ENABLED}" = "true" ]]; then ./makeSentryProperties.sh export SENTRY_PROPERTIES=sentry.properties - ../node_modules/sentry-cli-binary/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh + ../node_modules/sentry-cli-binary/bin/sentry-cli react-native xcode ./react-native-xcode.sh else echo "Sentry native integration is not enabled" - ../node_modules/react-native/scripts/react-native-xcode.sh + ./react-native-xcode.sh fi diff --git a/ios/react-native-xcode.sh b/ios/react-native-xcode.sh new file mode 100755 index 0000000000..c572f7187d --- /dev/null +++ b/ios/react-native-xcode.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# Copyright (c) 2015-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +# Bundle React Native app's code and image assets. +# This script is supposed to be invoked as part of Xcode build process +# and relies on environment variables (including PWD) set by Xcode + +# This scripts allows the app and app extension bundles to be shared or separated. +# Separating bundles allows for a minimal footprint for both app and app extension. +# The original script provided by RN does not bundle app extensions. + +# This way we can set the BundleEntryFilename to index.js for the main app and +# the BundleEntryFilename to share.ios.js for the extension + +DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH +MAIN_BUNDLE="main.jsbundle" +BUNDLE_FILE="$DEST/$MAIN_BUNDLE" +TMP_PATH="/tmp" +PLISTBUDDY='/usr/libexec/PlistBuddy' +PLIST=$TARGET_BUILD_DIR/$INFOPLIST_PATH + +[ -z "$SKIP_BUNDLING" ] && SKIP_BUNDLING=$($PLISTBUDDY -c "Print :BundleSkipped" "${PLIST}") +[ -z "$CP_BUNDLING" ] && CP_BUNDLING=$($PLISTBUDDY -c "Print :BundleCopied" "${PLIST}") + +if [[ "$SKIP_BUNDLING" && $SKIP_BUNDLING == "true" ]]; then + echo "SKIP_BUNDLING enabled; skipping." + if [[ "$CP_BUNDLING" && $CP_BUNDLING == "true" ]]; then + TMP_BUNDLE="$TMP_PATH/$MAIN_BUNDLE" + echo "CP_BUNDLING enabled; copying $TMP_BUNDLE to $DEST/" + if [ -f "$TMP_BUNDLE" ]; then + cp "$TMP_PATH/$MAIN_BUNDLE"* "$DEST/" + else + echo "CP_BUNDLING $TMP_BUNDLE does not exist!" + fi + fi + exit 0; +fi + +[ -z "$IS_DEV" ] && IS_DEV=$($PLISTBUDDY -c "Print :BundleDev" "${PLIST}") +[ -z "$FORCE_BUNDLING" ] && FORCE_BUNDLING=$($PLISTBUDDY -c "Print :BundleForced" "${PLIST}") + +if [ -z "$IS_DEV" ]; then + case "$CONFIGURATION" in + *Debug*) + if [[ "$PLATFORM_NAME" == *simulator ]]; then + if [[ "$FORCE_BUNDLING" && $FORCE_BUNDLING == "true" ]]; then + echo "FORCE_BUNDLING enabled; continuing to bundle." + else + echo "Skipping bundling in Debug for the Simulator (since the packager bundles for you). Use the FORCE_BUNDLING env flag or BundleForced plist key to change this behavior." + exit 0; + fi + else + echo "Bundling for physical device. Use the SKIP_BUNDLING flag to change this behavior." + fi + + DEV=true + ;; + "") + echo "$0 must be invoked by Xcode" + exit 1 + ;; + *) + DEV=false + ;; + esac +else + if [[ "$PLATFORM_NAME" == *simulator ]]; then + if [[ "$FORCE_BUNDLING" && $FORCE_BUNDLING == "true" ]]; then + echo "FORCE_BUNDLING enabled; continuing to bundle." + else + echo "Skipping bundling in Debug for the Simulator (since the packager bundles for you). Use the FORCE_BUNDLING flag to change this behavior." + exit 0; + fi + else + echo "Bundling for physical device. Use the SKIP_BUNDLING flag to change this behavior." + fi + DEV=$IS_DEV +fi + +# Path to react-native folder inside node_modules +# REACT_NATIVE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# Path to react-native folder inside src/native/utils/bin +REACT_NATIVE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../node_modules/react-native" && pwd)" +echo "REACT_NATIVE_DIR: $REACT_NATIVE_DIR" + +# Xcode project file for React Native apps is located in ios/ subfolder +cd "${REACT_NATIVE_DIR}"/../.. + +# Define NVM_DIR and source the nvm.sh setup script +[ -z "$NVM_DIR" ] && export NVM_DIR="$HOME/.nvm" + +# Define default ENTRY_FILENAME +[ -z "$ENTRY_FILENAME" ] && ENTRY_FILENAME=$($PLISTBUDDY -c "Print :BundleEntryFilename" "${PLIST}") +[ -z "$ENTRY_FILENAME" ] && ENTRY_FILENAME="index.js" +echo "ENTRY_FILENAME: $ENTRY_FILENAME" + +js_file_type=.js +ios_file_type=.ios.js +ios_file_name="${ENTRY_FILENAME/$js_file_type/$ios_file_type}" + +# Define entry file +if [[ -s $ios_file_name ]]; then + ENTRY_FILE=${1:-$ios_file_name} +else + ENTRY_FILE=${1:-$ENTRY_FILENAME} +fi + +if [[ -s "$HOME/.nvm/nvm.sh" ]]; then + . "$HOME/.nvm/nvm.sh" +elif [[ -x "$(command -v brew)" && -s "$(brew --prefix nvm)/nvm.sh" ]]; then + . "$(brew --prefix nvm)/nvm.sh" +fi + +# Set up the nodenv node version manager if present +if [[ -x "$HOME/.nodenv/bin/nodenv" ]]; then + eval "$("$HOME/.nodenv/bin/nodenv" init -)" +fi + +[ -z "$NODE_BINARY" ] && export NODE_BINARY="node" + +[ -z "$CLI_PATH" ] && export CLI_PATH="$REACT_NATIVE_DIR/local-cli/cli.js" + +nodejs_not_found() +{ + echo "error: Can't find '$NODE_BINARY' binary to build React Native bundle" >&2 + echo "If you have non-standard nodejs installation, select your project in Xcode," >&2 + echo "find 'Build Phases' - 'Bundle React Native code and images'" >&2 + echo "and change NODE_BINARY to absolute path to your node executable" >&2 + echo "(you can find it by invoking 'which node' in the terminal)" >&2 + exit 2 +} + +type $NODE_BINARY >/dev/null 2>&1 || nodejs_not_found + +# Print commands before executing them (useful for troubleshooting) +set -x +# DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH + +if [[ "$CONFIGURATION" = "Debug" && ! "$PLATFORM_NAME" == *simulator ]]; then + BUNDLE_SERVER=$($PLISTBUDDY -c "Print :BundleServer" "${PLIST}") + echo "BUNDLE_SERVER: ${BUNDLE_SERVER}" + if [ -z "$BUNDLE_SERVER" ]; then + IP=$(ipconfig getifaddr en0) + if [ -z "$IP" ]; then + IP=$(ifconfig | grep 'inet ' | grep -v ' 127.' | cut -d\ -f2 | awk 'NR==1{print $1}') + fi + else + IP=$BUNDLE_SERVER + fi + + if [ -z ${DISABLE_XIP+x} ]; then + IP="$IP.xip.io" + fi + + $PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:localhost:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" "$PLIST" + $PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:$IP:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" "$PLIST" + echo "$IP" > "$DEST/ip.txt" +fi + +$NODE_BINARY "$CLI_PATH" bundle \ + --entry-file "$ENTRY_FILE" \ + --platform ios \ + --dev $DEV \ + --reset-cache \ + --bundle-output "$BUNDLE_FILE" \ + --assets-dest "$DEST" + +if [[ $DEV != true && ! -f "$BUNDLE_FILE" ]]; then + echo "error: File $BUNDLE_FILE does not exist. This must be a bug with" >&2 + echo "React Native, please report it here: https://github.com/facebook/react-native/issues" + exit 2 +else + cp "$BUNDLE_FILE"* $TMP_PATH + if [[ $DEV == "true" ]]; then + if nc -w 5 -z localhost 8081 ; then + if ! curl -s "http://localhost:8081/status" | grep -q "packager-status:running"; then + echo "Port 8081 already in use, packager is either not running or not running correctly" + exit 0 + fi + else + open "$REACT_NATIVE_DIR/scripts/launchPackager.command" || echo "Can't start packager automatically" + fi + fi +fi diff --git a/share.ios.js b/share.ios.js new file mode 100644 index 0000000000..8946251e0c --- /dev/null +++ b/share.ios.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {AppRegistry} from 'react-native'; + +import ShareExtension from 'share_extension/ios'; + +AppRegistry.registerComponent('MattermostShare', () => ShareExtension); diff --git a/share_extension/icons/channel_type.js b/share_extension/icons/channel_type.js new file mode 100644 index 0000000000..caf60f50e0 --- /dev/null +++ b/share_extension/icons/channel_type.js @@ -0,0 +1,73 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import { + View +} from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; + +import {Preferences} from 'mattermost-redux/constants'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; + +const defaultTheme = Preferences.THEMES.default; + +export function PublicChannel() { + return ( + + + + ); +} + +export function PrivateChannel() { + return ( + + + + ); +} + +export function DirectChannel() { + return ( + + + + ); +} + +export function GroupChannel() { + return ( + + + + ); +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + height: 16, + marginRight: 5, + width: 16 + }, + icon: { + color: theme.centerChannelColor, + fontSize: 16 + } + }; +}); + +const style = getStyleSheet(defaultTheme); diff --git a/share_extension/icons/excel.js b/share_extension/icons/excel.js new file mode 100644 index 0000000000..e3354b31d2 --- /dev/null +++ b/share_extension/icons/excel.js @@ -0,0 +1,55 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import Svg, { + G, + Path +} from 'react-native-svg'; + +function ExcelSvg({height, width}) { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} + +ExcelSvg.propTypes = { + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired +}; + +export default ExcelSvg; diff --git a/share_extension/icons/generic.js b/share_extension/icons/generic.js new file mode 100644 index 0000000000..7a35de9269 --- /dev/null +++ b/share_extension/icons/generic.js @@ -0,0 +1,58 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import Svg, { + G, + Path +} from 'react-native-svg'; + +function GenericSvg({height, width}) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +GenericSvg.propTypes = { + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired +}; + +export default GenericSvg; diff --git a/share_extension/icons/pdf.js b/share_extension/icons/pdf.js new file mode 100644 index 0000000000..ff4458d56a --- /dev/null +++ b/share_extension/icons/pdf.js @@ -0,0 +1,53 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import Svg, { + G, + Path +} from 'react-native-svg'; + +function PdfSvg({height, width}) { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +PdfSvg.propTypes = { + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired +}; + +export default PdfSvg; diff --git a/share_extension/icons/ppt.js b/share_extension/icons/ppt.js new file mode 100644 index 0000000000..dcd0ba965b --- /dev/null +++ b/share_extension/icons/ppt.js @@ -0,0 +1,55 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import Svg, { + G, + Path +} from 'react-native-svg'; + +function PptSvg({height, width}) { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} + +PptSvg.propTypes = { + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired +}; + +export default PptSvg; diff --git a/share_extension/icons/zip.js b/share_extension/icons/zip.js new file mode 100644 index 0000000000..0c4ef4cd48 --- /dev/null +++ b/share_extension/icons/zip.js @@ -0,0 +1,68 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import Svg, { + G, + Path +} from 'react-native-svg'; + +function ZipSvg({height, width}) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +ZipSvg.propTypes = { + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired +}; + +export default ZipSvg; diff --git a/share_extension/ios/extension_channel_item.js b/share_extension/ios/extension_channel_item.js new file mode 100644 index 0000000000..8d3a4fef4b --- /dev/null +++ b/share_extension/ios/extension_channel_item.js @@ -0,0 +1,121 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + TouchableHighlight, + Text, + View +} from 'react-native'; +import IonIcon from 'react-native-vector-icons/Ionicons'; + +import {wrapWithPreventDoubleTap} from 'app/utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +import {DirectChannel, GroupChannel, PublicChannel, PrivateChannel} from 'share_extension/icons/channel_type'; + +const channelTypes = { + D: DirectChannel, + G: GroupChannel, + O: PublicChannel, + P: PrivateChannel +}; + +export default class ExtensionChannelItem extends PureComponent { + static propTypes = { + channel: PropTypes.object.isRequired, + currentChannelId: PropTypes.string.isRequired, + onSelectChannel: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + }; + + onPress = wrapWithPreventDoubleTap(() => { + const {channel, onSelectChannel} = this.props; + requestAnimationFrame(() => { + onSelectChannel(channel); + }); + }); + + render() { + const { + channel, + currentChannelId, + theme + } = this.props; + + const style = getStyleSheet(theme); + const isCurrent = channel.id === currentChannelId; + let current; + + if (isCurrent) { + current = ( + + + + ); + } + + const Type = channelTypes[channel.type] || PublicChannel; + const icon = ; + + return ( + + + + {icon} + + {channel.display_name} + + {current} + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + flexDirection: 'row', + height: 45, + paddingHorizontal: 15 + }, + item: { + alignItems: 'center', + height: 45, + flex: 1, + flexDirection: 'row' + }, + text: { + color: theme.centerChannelColor, + flex: 1, + fontSize: 16, + fontWeight: '600', + lineHeight: 16, + paddingRight: 5 + }, + iconContainer: { + marginRight: 5 + }, + checkmarkContainer: { + alignItems: 'flex-end' + }, + checkmark: { + color: theme.linkColor, + fontSize: 16 + } + }; +}); diff --git a/share_extension/ios/extension_channels.js b/share_extension/ios/extension_channels.js new file mode 100644 index 0000000000..dca7b0891d --- /dev/null +++ b/share_extension/ios/extension_channels.js @@ -0,0 +1,363 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + ActivityIndicator, + SectionList, + 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, Preferences} from 'mattermost-redux/constants'; +import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils'; +import {displayUsername} from 'mattermost-redux/utils/user_utils'; + +import SearchBar from 'app/components/search_bar'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +import ExtensionChannelItem from './extension_channel_item'; +import ExtensionNavBar from './extension_nav_bar'; + +export default class ExtensionChannels extends PureComponent { + static propTypes = { + currentChannelId: PropTypes.string.isRequired, + currentUserId: PropTypes.string.isRequired, + navigator: PropTypes.object.isRequired, + onSelectChannel: PropTypes.func.isRequired, + teamId: PropTypes.string.isRequired, + theme: PropTypes.object.isRequired, + title: PropTypes.string.isRequired + }; + + static contextTypes = { + intl: intlShape + }; + + state = { + sections: null + }; + + componentWillMount() { + this.loadChannels(); + } + + buildSections = (term) => { + const {channels} = this.state; + const sections = []; + const publicChannels = []; + const privateChannels = []; + const directChannels = []; + + channels.forEach((channel) => { + const include = term ? channel.display_name.toLowerCase().includes(term.toLowerCase()) : true; + if (channel.display_name && include) { + switch (channel.type) { + case General.OPEN_CHANNEL: + publicChannels.push(channel); + break; + case General.PRIVATE_CHANNEL: + privateChannels.push(channel); + break; + default: + directChannels.push(channel); + break; + } + } + }); + + if (publicChannels.length) { + sections.push({ + id: 'sidebar.channels', + defaultMessage: 'PUBLIC CHANNELS', + data: publicChannels.sort(this.sortDisplayName) + }); + } + + if (privateChannels.length) { + sections.push({ + id: 'sidebar.pg', + defaultMessage: 'PRIVATE CHANNELS', + data: privateChannels.sort(this.sortDisplayName) + }); + } + + if (directChannels.length) { + sections.push({ + id: 'sidebar.direct', + defaultMessage: 'DIRECT MESSAGES', + data: directChannels.sort(this.sortDisplayName) + }); + } + + 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(); + }; + + keyExtractor = (item) => item.id; + + loadChannels = async () => { + try { + const {currentUserId, teamId} = this.props; + const channelsMap = {}; + + 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); + 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; + }); + + const dmProfiles = await Client4.getProfilesByIds(usersInDms); + + for (let i = 0; i < dms.length; i++) { + const channel = dms[i]; + if (channel.type === General.DM_CHANNEL) { + 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); + } + } + + const channels = myChannels.map((channel) => { + return { + id: channel.id, + display_name: channelsMap[channel.id] || channel.display_name, + type: channel.type + }; + }); + + this.setState({ + channels + }, () => { + this.buildSections(); + }); + } catch (error) { + this.setState({error}); + } + }; + + handleSearch = (term) => { + this.setState({term}, () => { + if (this.throttleTimeout) { + clearTimeout(this.throttleTimeout); + } + + this.throttleTimeout = setTimeout(() => { + this.buildSections(term); + }, 300); + }); + }; + + handleSelectChannel = (channel) => { + this.props.onSelectChannel(channel); + this.goBack(); + }; + + renderBody = (styles) => { + const {error, sections} = this.state; + + if (error) { + return ( + + + {error.message} + + + ); + } + + if (!sections) { + return ( + + + + ); + } + + return ( + + ); + }; + + renderItem = ({item}) => { + const {currentChannelId, theme} = this.props; + + return ( + + ); + }; + + renderItemSeparator = () => { + const {theme} = this.props; + const styles = getStyleSheet(theme); + + return ( + + + + ); + }; + + renderSearchBar = (styles) => { + const {formatMessage} = this.context.intl; + const {theme} = this.props; + + return ( + + + + ); + }; + + renderSectionHeader = ({section}) => { + const {intl} = this.context; + const {theme} = this.props; + const styles = getStyleSheet(theme); + const { + defaultMessage, + id + } = section; + + return ( + + + + {intl.formatMessage({id, defaultMessage}).toUpperCase()} + + + + ); + }; + + sort = (a, b) => { + const locale = DeviceInfo.getDeviceLocale().split('-')[0]; + return a.localeCompare(b, locale, {numeric: true}); + }; + + sortDisplayName = (a, b) => { + const locale = DeviceInfo.getDeviceLocale().split('-')[0]; + return a.display_name.localeCompare(b.display_name, locale, {numeric: true}); + }; + + render() { + const {theme, title} = this.props; + const styles = getStyleSheet(theme); + + return ( + + + {this.renderBody(styles)} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + flex: { + flex: 1 + }, + separatorContainer: { + paddingLeft: 35 + }, + separator: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + height: 1 + }, + loadingContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center' + }, + searchContainer: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + paddingBottom: 2 + }, + searchBarInput: { + backgroundColor: '#fff', + color: theme.centerChannelColor, + fontSize: 15 + }, + titleContainer: { + height: 30 + }, + title: { + color: changeOpacity(theme.centerChannelColor, 0.6), + fontSize: 15, + lineHeight: 30, + paddingHorizontal: 15 + }, + errorContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + paddingHorizontal: 15 + }, + error: { + color: theme.errorTextColor, + fontSize: 14 + } + }; +}); diff --git a/share_extension/ios/extension_nav_bar.js b/share_extension/ios/extension_nav_bar.js new file mode 100644 index 0000000000..fa3510f213 --- /dev/null +++ b/share_extension/ios/extension_nav_bar.js @@ -0,0 +1,149 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Text, TouchableOpacity, View} from 'react-native'; +import IonIcon from 'react-native-vector-icons/Ionicons'; + +import {emptyFunction} from 'app/utils/general'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class ExtensionNavBar extends PureComponent { + static propTypes = { + authenticated: PropTypes.bool, + backButton: PropTypes.bool, + leftButtonTitle: PropTypes.string, + onLeftButtonPress: PropTypes.func, + onRightButtonPress: PropTypes.func, + rightButtonTitle: PropTypes.string, + theme: PropTypes.object.isRequired, + title: PropTypes.string + }; + + static defaultProps = { + backButton: false, + onLeftButtonPress: emptyFunction, + title: 'Mattermost' + }; + + renderLeftButton = (styles) => { + const {backButton, leftButtonTitle, onLeftButtonPress} = this.props; + let backComponent; + if (backButton) { + backComponent = ( + + ); + } else if (leftButtonTitle) { + backComponent = ( + + {leftButtonTitle} + + ); + } + + if (backComponent) { + return ( + + {backComponent} + + ); + } + + return ; + }; + + renderRightButton = (styles) => { + const {authenticated, onRightButtonPress, rightButtonTitle} = this.props; + + if (rightButtonTitle && authenticated) { + return ( + + + {rightButtonTitle} + + + ); + } + + return ; + }; + + render() { + const {theme, title} = this.props; + const styles = getStyleSheet(theme); + + return ( + + {this.renderLeftButton(styles)} + + + {title} + + + {this.renderRightButton(styles)} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2), + borderBottomWidth: 1, + flexDirection: 'row', + height: 45 + }, + backButtonContainer: { + justifyContent: 'center', + paddingHorizontal: 15, + width: '30%' + }, + titleContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center' + }, + backButton: { + color: theme.linkColor, + fontSize: 34 + }, + leftButton: { + color: theme.linkColor, + fontSize: 16 + }, + title: { + fontSize: 17, + fontWeight: '600' + }, + rightButtonContainer: { + alignItems: 'flex-end', + justifyContent: 'center', + paddingHorizontal: 15, + width: '30%' + }, + rightButton: { + color: theme.linkColor, + fontSize: 16, + fontWeight: '600' + } + }; +}); diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js new file mode 100644 index 0000000000..9a3cbe2146 --- /dev/null +++ b/share_extension/ios/extension_post.js @@ -0,0 +1,644 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {intlShape} from 'react-intl'; +import { + Dimensions, + Image, + NativeModules, + ScrollView, + Text, + TextInput, + TouchableHighlight, + View +} from 'react-native'; +import IonIcon from 'react-native-vector-icons/Ionicons'; +import Video from 'react-native-video'; +import LocalAuth from 'react-native-local-auth'; + +import {Client4} from 'mattermost-redux/client'; +import {lookupMimeType} from 'mattermost-redux/utils/file_utils'; + +import mattermostBucket from 'app/mattermost_bucket'; +import {generateId} from 'app/utils/file'; +import {wrapWithPreventDoubleTap} from 'app/utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import Config from 'assets/config'; + +import ExcelSvg from 'share_extension/icons/excel'; +import GenericSvg from 'share_extension/icons/generic'; +import PdfSvg from 'share_extension/icons/pdf'; +import PptSvg from 'share_extension/icons/ppt'; +import ZipSvg from 'share_extension/icons/zip'; + +import ExtensionChannels from './extension_channels'; +import ExtensionNavBar from './extension_nav_bar'; +import ExtensionTeams from './extension_teams'; + +const ShareExtension = NativeModules.MattermostShare; +const MAX_INPUT_HEIGHT = 95; +const MAX_MESSAGE_LENGTH = 4000; + +const extensionSvg = { + csv: ExcelSvg, + pdf: PdfSvg, + ppt: PptSvg, + pptx: PptSvg, + xls: ExcelSvg, + xlsx: ExcelSvg, + zip: ZipSvg +}; + +export default class ExtensionPost extends PureComponent { + static propTypes = { + credentials: PropTypes.object, + navigator: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + }; + + static contextTypes = { + intl: intlShape + }; + + constructor(props, context) { + super(props, context); + + const {height, width} = Dimensions.get('window'); + const isLandscape = width > height; + + this.state = { + currentUserId: null, + error: null, + files: [], + isLandscape, + value: '' + }; + } + + componentWillMount() { + this.loadData(); + } + + componentDidMount() { + this.focusInput(); + } + + componentDidUpdate() { + this.focusInput(); + } + + auth = async () => { + try { + const emmSecured = await mattermostBucket.get('emm', Config.AppGroupId); + if (emmSecured) { + const {intl} = this.context; + await LocalAuth.authenticate({ + reason: intl.formatMessage({ + id: 'mobile.managed.secured_by', + defaultMessage: 'Secured by {vendor}' + }, {emmSecured}), + fallbackToPasscode: true, + suppressEnterPassword: true + }); + } + } catch (error) { + this.props.onClose(); + } + }; + + focusInput = () => { + if (this.input && !this.input.isFocused()) { + this.input.focus(); + } + }; + + getInputRef = (ref) => { + this.input = ref; + }; + + getScrollViewRef = (ref) => { + this.scrollView = ref; + }; + + goToChannels = wrapWithPreventDoubleTap(() => { + const {navigator, theme} = this.props; + const {channel, currentUserId, team} = this.state; + + navigator.push({ + component: ExtensionChannels, + wrapperStyle: { + borderRadius: 10, + backgroundColor: theme.centerChannelBg + }, + passProps: { + currentChannelId: channel.id, + currentUserId, + onSelectChannel: this.selectChannel, + teamId: team.id, + theme, + title: team.display_name + } + }); + }); + + goToTeams = wrapWithPreventDoubleTap(() => { + const {navigator, theme} = this.props; + const {formatMessage} = this.context.intl; + const {team} = this.state; + + navigator.push({ + component: ExtensionTeams, + title: formatMessage({id: 'quick_switch_modal.teams', defaultMessage: 'Teams'}), + wrapperStyle: { + borderRadius: 10, + backgroundColor: theme.centerChannelBg + }, + passProps: { + currentTeamId: team.id, + onSelectTeam: this.selectTeam, + theme + } + }); + }); + + handleCancel = wrapWithPreventDoubleTap(() => { + this.props.onClose(); + }); + + handleTextChange = (value) => { + this.setState({value}); + }; + + loadData = async () => { + const {credentials} = this.props; + if (credentials) { + 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 items = await ShareExtension.data(Config.AppGroupId); + const text = []; + const urls = []; + const files = []; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + switch (item.type) { + case 'public.plain-text': + text.push(item.value); + break; + case 'public.url': + urls.push(item.value); + break; + default: { + const fullPath = item.value; + const filePath = fullPath.replace('file://', ''); + const filename = fullPath.replace(/^.*[\\/]/, ''); + const extension = filename.split('.').pop(); + files.push({ + extension, + filename, + filePath, + fullPath, + mimeType: lookupMimeType(filename.toLowerCase()), + type: item.type + }); + break; + } + } + } + + let value = text.join('\n'); + if (urls.length) { + value += text.length ? `\n${urls.join('\n')}` : urls.join('\n'); + } + + Client4.setUrl(credentials.url); + Client4.setToken(credentials.token); + this.setState({channel, currentUserId, files, team, value}); + } catch (error) { + this.setState({error}); + } + } + }; + + onLayout = async () => { + const isLandscape = await ShareExtension.getOrientation() === 'LANDSCAPE'; + + if (this.state.isLandscape !== isLandscape) { + if (this.scrollView) { + setTimeout(() => { + this.scrollView.scrollTo({y: 0, animated: false}); + }, 250); + } + this.setState({isLandscape}); + } + }; + + renderBody = (styles) => { + const {formatMessage} = this.context.intl; + const {credentials, theme} = this.props; + const {error, value} = this.state; + + if (credentials && !error) { + return ( + + + {this.renderFiles(styles)} + + ); + } + + if (error) { + return ( + + + {error.message} + + + ); + } + + return ( + + + {'Authentication required: Please first login using the app.'} + + + ); + }; + + renderChannelButton = (styles) => { + const {formatMessage} = this.context.intl; + const {credentials, theme} = this.props; + const {channel} = this.state; + const channelName = channel ? channel.display_name : ''; + + if (!credentials) { + return null; + } + + return ( + + + + + {formatMessage({id: 'mobile.share_extension.channel', defaultMessage: 'Channel'})} + + + + + {channelName} + + + + + + + + ); + }; + + renderFiles = (styles) => { + const {files} = this.state; + + return files.map((file, index) => { + let component; + + switch (file.type) { + case 'public.image': + component = ( + + + + ); + break; + case 'public.movie': + component = ( + + + ); + break; + case 'public.file-url': { + let SvgIcon = extensionSvg[file.extension]; + if (!SvgIcon) { + SvgIcon = GenericSvg; + } + + component = ( + + + + + + + + ); + break; + } + } + + return ( + + {component} + + {file.filename} + + + ); + }); + }; + + renderTeamButton = (styles) => { + const {formatMessage} = this.context.intl; + const {credentials, theme} = this.props; + const {team} = this.state; + const teamName = team ? team.display_name : ''; + + if (!credentials) { + return null; + } + + return ( + + + + + {formatMessage({id: 'mobile.share_extension.team', defaultMessage: 'Team'})} + + + + + {teamName} + + + + + + + + ); + }; + + selectChannel = (channel) => { + this.setState({channel}); + }; + + selectTeam = (team, channel) => { + this.setState({channel, team}); + }; + + sendMessage = wrapWithPreventDoubleTap(async () => { + const {credentials, onClose} = this.props; + const {channel, currentUserId, files, value} = this.state; + + // If no text and no files do nothing + if (!value && !files.length) { + return; + } + + if (currentUserId && credentials) { + await this.auth(); + + try { + const post = { + user_id: currentUserId, + channel_id: channel.id, + message: value + }; + + const data = { + files, + post, + requestId: generateId() + }; + + onClose(data); + } catch (error) { + this.setState({error}); + setTimeout(() => { + onClose(); + }, 5000); + } + } + }); + + render() { + const {credentials, theme} = this.props; + const {formatMessage} = this.context.intl; + const styles = getStyleSheet(theme); + + return ( + + + {this.renderBody(styles)} + {this.renderTeamButton(styles)} + {this.renderChannelButton(styles)} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + flex: { + flex: 1 + }, + container: { + flex: 1, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.05) + }, + input: { + color: theme.centerChannelColor, + fontSize: 17, + marginBottom: 5, + width: '100%' + }, + divider: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), + height: 1, + marginVertical: 5, + width: '100%' + }, + scrollView: { + paddingHorizontal: 15 + }, + buttonContainer: { + borderTopColor: changeOpacity(theme.centerChannelColor, 0.2), + borderTopWidth: 1, + height: 45, + paddingHorizontal: 15 + }, + buttonWrapper: { + alignItems: 'center', + flex: 1, + flexDirection: 'row' + }, + buttonLabelContainer: { + flex: 1 + }, + buttonLabel: { + fontSize: 17, + lineHeight: 45 + }, + buttonValueContainer: { + justifyContent: 'flex-end', + flex: 1, + flexDirection: 'row' + }, + buttonValue: { + color: changeOpacity(theme.centerChannelColor, 0.4), + alignSelf: 'flex-end', + fontSize: 17, + lineHeight: 45 + }, + arrowContainer: { + height: 45, + justifyContent: 'center', + marginLeft: 15, + top: 2 + }, + unauthenticatedContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + paddingHorizontal: 15 + }, + unauthenticated: { + color: theme.errorTextColor, + fontSize: 14 + }, + fileContainer: { + alignItems: 'center', + backgroundColor: theme.centerChannelBg, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + borderRadius: 4, + borderWidth: 1, + flexDirection: 'row', + height: 48, + marginBottom: 10, + width: '100%' + }, + filename: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 13, + flex: 1 + }, + otherContainer: { + borderBottomLeftRadius: 4, + borderTopLeftRadius: 4, + height: 48, + marginRight: 10, + paddingVertical: 10, + width: 38 + }, + otherWrapper: { + borderRightWidth: 1, + borderRightColor: changeOpacity(theme.centerChannelColor, 0.2), + flex: 1 + }, + fileIcon: { + alignItems: 'center', + justifyContent: 'center', + flex: 1 + }, + imageContainer: { + borderBottomLeftRadius: 4, + borderTopLeftRadius: 4, + height: 48, + marginRight: 10, + width: 38 + }, + image: { + alignItems: 'center', + height: 48, + justifyContent: 'center', + overflow: 'hidden', + width: 38 + }, + video: { + backgroundColor: theme.centerChannelBg, + alignItems: 'center', + height: 48, + justifyContent: 'center', + overflow: 'hidden', + width: 38 + } + }; +}); diff --git a/share_extension/ios/extension_team_item.js b/share_extension/ios/extension_team_item.js new file mode 100644 index 0000000000..c09d5e4bf9 --- /dev/null +++ b/share_extension/ios/extension_team_item.js @@ -0,0 +1,125 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Text, + TouchableHighlight, + View +} from 'react-native'; +import IonIcon from 'react-native-vector-icons/Ionicons'; + +import {wrapWithPreventDoubleTap} from 'app/utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class TeamsListItem extends React.PureComponent { + static propTypes = { + currentTeamId: PropTypes.string.isRequired, + onSelectTeam: PropTypes.func.isRequired, + team: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired + }; + + onPress = wrapWithPreventDoubleTap(() => { + const {onSelectTeam, team} = this.props; + onSelectTeam(team); + }); + + render() { + const { + currentTeamId, + team, + theme + } = this.props; + const styles = getStyleSheet(theme); + + let current; + if (team.id === currentTeamId) { + current = ( + + + + ); + } + + const icon = ( + + + {team.display_name.substr(0, 2).toUpperCase()} + + + ); + + return ( + + + + {icon} + + {team.display_name} + + {current} + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + flexDirection: 'row', + height: 45, + paddingHorizontal: 15 + }, + item: { + alignItems: 'center', + height: 45, + flex: 1, + flexDirection: 'row' + }, + text: { + color: theme.centerChannelColor, + flex: 1, + fontSize: 16, + fontWeight: '600', + lineHeight: 16, + paddingRight: 5 + }, + iconContainer: { + alignItems: 'center', + backgroundColor: theme.linkColor, + borderRadius: 2, + height: 30, + justifyContent: 'center', + width: 30, + marginRight: 10 + }, + icon: { + color: theme.sidebarText, + fontFamily: 'OpenSans', + fontSize: 15, + fontWeight: '600' + }, + checkmarkContainer: { + alignItems: 'flex-end' + }, + checkmark: { + color: theme.linkColor, + fontSize: 16 + } + }; +}); diff --git a/share_extension/ios/extension_teams.js b/share_extension/ios/extension_teams.js new file mode 100644 index 0000000000..c9c841b251 --- /dev/null +++ b/share_extension/ios/extension_teams.js @@ -0,0 +1,209 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +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 {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +import ExtensionNavBar from './extension_nav_bar'; +import ExtensionTeamItem from './extension_team_item'; + +export default class ExtensionTeams extends PureComponent { + static propTypes = { + currentTeamId: PropTypes.string.isRequired, + navigator: PropTypes.object.isRequired, + onSelectTeam: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + }; + + static contextTypes = { + intl: intlShape + }; + + state = { + defaultChannels: null, + error: null, + myTeams: null + }; + + componentWillMount() { + this.loadTeams(); + } + + goBack = () => { + this.props.navigator.pop(); + }; + + handleSelectTeam = (team) => { + const {defaultChannels} = this.state; + const townSquare = defaultChannels[team.id]; + this.props.onSelectTeam(team, townSquare); + this.goBack(); + }; + + keyExtractor = (item) => item.id; + + loadTeams = async () => { + try { + const defaultChannels = {}; + const teams = await Client4.getMyTeams(); + const myMembers = await Client4.getMyTeamMembers(); + const myTeams = []; + + teams.forEach(async (team) => { + 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, + myTeams: myTeams.sort(this.sortDisplayName) + }); + } catch (error) { + this.setState({error}); + } + }; + + renderBody = (styles) => { + const {error, myTeams} = this.state; + + if (error) { + return ( + + + {error.message} + + + ); + } + + if (!myTeams) { + return ( + + + + ); + } + + return ( + + ); + }; + + renderItem = ({item}) => { + const {currentTeamId, theme} = this.props; + + return ( + + ); + }; + + renderItemSeparator = () => { + const {theme} = this.props; + const styles = getStyleSheet(theme); + + return ( + + + + ); + }; + + sortDisplayName = (a, b) => { + const locale = DeviceInfo.getDeviceLocale().split('-')[0]; + return a.display_name.localeCompare(b.display_name, locale, {numeric: true}); + }; + + render() { + const {formatMessage} = this.context.intl; + const {theme} = this.props; + const styles = getStyleSheet(theme); + + return ( + + + {this.renderBody(styles)} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + flex: { + flex: 1 + }, + separatorContainer: { + paddingLeft: 60 + }, + separator: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + height: 1 + }, + loadingContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center' + }, + searchContainer: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + paddingBottom: 2 + }, + searchBarInput: { + backgroundColor: '#fff', + color: theme.centerChannelColor, + fontSize: 15 + }, + titleContainer: { + height: 30 + }, + title: { + color: changeOpacity(theme.centerChannelColor, 0.6), + fontSize: 15, + lineHeight: 30, + paddingHorizontal: 15 + }, + errorContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + paddingHorizontal: 15 + }, + error: { + color: theme.errorTextColor, + fontSize: 14 + } + }; +}); diff --git a/share_extension/ios/index.js b/share_extension/ios/index.js new file mode 100644 index 0000000000..229390e1f1 --- /dev/null +++ b/share_extension/ios/index.js @@ -0,0 +1,170 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import {IntlProvider} from 'react-intl'; +import DeviceInfo from 'react-native-device-info'; +import { + Animated, + Dimensions, + NativeModules, + NavigatorIOS, + StyleSheet, + View +} from 'react-native'; + +import {Preferences} from 'mattermost-redux/constants'; + +import {getTranslations} from 'app/i18n'; +import mattermostBucket from 'app/mattermost_bucket'; +import Config from 'assets/config'; + +import ExtensionPost from './extension_post'; + +const {View: AnimatedView} = Animated; +const ShareExtension = NativeModules.MattermostShare; + +export default class SharedApp extends PureComponent { + constructor(props) { + super(props); + + const {height, width} = Dimensions.get('window'); + const isLandscape = width > height; + + this.state = { + backdropOpacity: new Animated.Value(0), + containerOpacity: new Animated.Value(0), + isLandscape + }; + + mattermostBucket.get('credentials', Config.AppGroupId).then((value) => { + this.credentials = value; + this.setState({init: true}); + }); + } + + componentDidMount() { + Animated.parallel([ + Animated.timing( + this.state.backdropOpacity, + { + toValue: 0.5, + duration: 100 + }), + Animated.timing( + this.state.containerOpacity, + { + toValue: 1, + duration: 250 + }) + ]).start(); + } + + 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); + }); + }; + + onLayout = (e) => { + const {height, width} = e.nativeEvent.layout; + const isLandscape = width > height; + if (this.state.isLandscape !== isLandscape) { + this.setState({isLandscape}); + } + }; + + render() { + const {init, isLandscape} = this.state; + + if (!init) { + return null; + } + + const theme = Preferences.THEMES.default; + const locale = DeviceInfo.getDeviceLocale().split('-')[0]; + + const initialRoute = { + component: ExtensionPost, + title: 'Mattermost', + passProps: { + credentials: this.credentials, + onClose: this.onClose, + isLandscape, + theme + }, + wrapperStyle: { + borderRadius: 10, + backgroundColor: theme.centerChannelBg + } + }; + + return ( + + + + + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + flex: { + flex: 1 + }, + backdrop: { + position: 'absolute', + flex: 1, + backgroundColor: '#000', + height: '100%', + width: '100%' + }, + wrapper: { + flex: 1, + marginHorizontal: 20 + }, + container: { + backgroundColor: 'white', + borderRadius: 10, + position: 'relative', + width: '100%' + } +}); diff --git a/test/setup.js b/test/setup.js index da09c0ef32..2755ba161e 100644 --- a/test/setup.js +++ b/test/setup.js @@ -29,6 +29,9 @@ mockery.registerMock('react-native', { addEventListener: () => true, fetch: () => Promise.resolve(true) } + }, + Platform: { + OS: 'ios' } }); mockery.registerMock('react-native-device-info', {