diff --git a/.eslintrc.json b/.eslintrc.json
index 5890e26bb..1ab3fb4e9 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -259,4 +259,4 @@
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
"mocha/no-exclusive-tests": 2
}
-}
+}
\ No newline at end of file
diff --git a/app/utils/sentry/index.js b/app/utils/sentry/index.js
index 223938812..069619bed 100644
--- a/app/utils/sentry/index.js
+++ b/app/utils/sentry/index.js
@@ -11,6 +11,7 @@ import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeam, getCurrentTeamMembership} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
+export const LOGGER_EXTENSION = 'extension';
export const LOGGER_JAVASCRIPT = 'javascript';
export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning';
export const LOGGER_NATIVE = 'native';
@@ -49,6 +50,18 @@ export function captureException(error, logger, store) {
}, store);
}
+export function captureExceptionWithoutState(err, logger) {
+ if (!Config.SentryEnabled) {
+ return;
+ }
+
+ try {
+ Sentry.captureException(err, {logger});
+ } catch (error) {
+ // do nothing...
+ }
+}
+
export function captureMessage(message, logger, store) {
capture(() => {
Sentry.captureMessage(message, {logger});
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 3f2850958..3f20ad505 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -2269,6 +2269,9 @@
"mobile.settings.team_selection": "Team Selection",
"mobile.share_extension.cancel": "Cancel",
"mobile.share_extension.channel": "Channel",
+ "mobile.share_extension.error_message": "An error has occurred while using the share extension.",
+ "mobile.share_extension.error_close": "Close",
+ "mobile.share_extension.error_title": "Extension Error",
"mobile.share_extension.send": "Send",
"mobile.share_extension.team": "Team",
"mobile.suggestion.members": "Members",
diff --git a/share_extension/android/extension.js b/share_extension/android/extension.js
new file mode 100644
index 000000000..6fa205282
--- /dev/null
+++ b/share_extension/android/extension.js
@@ -0,0 +1,58 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {Alert, NativeModules} from 'react-native';
+import {intlShape} from 'react-intl';
+
+import {captureException, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry';
+
+import Navigation from './navigation';
+
+const ShareExtension = NativeModules.MattermostShare;
+
+export default class ShareApp extends PureComponent {
+ static contextTypes = {
+ intl: intlShape,
+ store: PropTypes.object.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ initializeSentry();
+ }
+
+ componentDidCatch(error) {
+ const {intl, store} = this.context;
+ const {formatMessage} = intl;
+
+ captureException(error, LOGGER_EXTENSION, store);
+
+ Alert.alert(
+ formatMessage({
+ id: 'mobile.share_extension.error_title',
+ defaultMessage: 'Extension Error',
+ }),
+ formatMessage({
+ id: 'mobile.share_extension.error_message',
+ defaultMessage: 'An error has occurred while using the share extension.',
+ }),
+ [
+ {
+ text: formatMessage({
+ id: 'mobile.share_extension.error_close',
+ defaultMessage: 'Close',
+ }),
+ onPress: this.close,
+ },
+ ]
+ );
+ }
+
+ close = () => ShareExtension.close(null)
+
+ render() {
+ return ;
+ }
+}
diff --git a/share_extension/android/index.js b/share_extension/android/index.js
index 61e67db4d..735cee6e8 100644
--- a/share_extension/android/index.js
+++ b/share_extension/android/index.js
@@ -13,7 +13,7 @@ import {getCurrentLocale} from 'app/selectors/i18n';
import configureStore from 'app/store';
import {extensionSelectTeamId} from './actions';
-import Navigation from './navigation';
+import Extension from './extension';
export default class ShareApp extends PureComponent {
constructor() {
@@ -54,7 +54,7 @@ export default class ShareApp extends PureComponent {
locale={locale}
messages={getTranslations(locale)}
>
-
+
);
diff --git a/share_extension/ios/error_message.js b/share_extension/ios/error_message.js
new file mode 100644
index 000000000..f104721ed
--- /dev/null
+++ b/share_extension/ios/error_message.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {TouchableOpacity, View} from 'react-native';
+
+import {Preferences} from 'mattermost-redux/constants';
+
+import FormattedText from 'app/components/formatted_text';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default function errorMessage(props) {
+ const {close} = props;
+ const theme = Preferences.THEMES.default;
+ const styles = getStyleSheet(theme);
+
+ return (
+
+
+
+
+
+
+ close()}
+ >
+
+
+
+
+
+ );
+}
+
+errorMessage.propTypes = {
+ close: PropTypes.func.isRequired,
+ formatMessage: PropTypes.func.isRequired,
+};
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ errorButton: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderTopWidth: 2,
+ borderTopColor: changeOpacity(theme.linkColor, 0.3),
+ paddingVertical: 15,
+ },
+ errorButtonText: {
+ color: changeOpacity(theme.linkColor, 0.7),
+ fontSize: 18,
+ },
+ errorContainer: {
+ borderRadius: 5,
+ backgroundColor: 'white',
+ marginHorizontal: 35,
+ },
+ errorContent: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
+ },
+ errorMessage: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 25,
+ },
+ errorMessageText: {
+ color: theme.centerChannelColor,
+ fontSize: 16,
+ textAlign: 'center',
+ },
+ errorWrapper: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.5),
+ },
+ };
+});
diff --git a/share_extension/ios/extension.js b/share_extension/ios/extension.js
new file mode 100644
index 000000000..686250c21
--- /dev/null
+++ b/share_extension/ios/extension.js
@@ -0,0 +1,150 @@
+// 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 {Animated, Dimensions, NavigatorIOS, StyleSheet, View} from 'react-native';
+
+import {Preferences} from 'mattermost-redux/constants';
+
+import mattermostBucket from 'app/mattermost_bucket';
+
+import ExtensionPost from './extension_post';
+
+const {View: AnimatedView} = Animated;
+
+export default class SharedApp extends PureComponent {
+ static propTypes = {
+ appGroupId: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ };
+
+ 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.readFromFile('entities', props.appGroupId).then((value) => {
+ this.entities = 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();
+ }
+
+ onLayout = (e) => {
+ const {height, width} = e.nativeEvent.layout;
+ const isLandscape = width > height;
+ if (this.state.isLandscape !== isLandscape) {
+ this.setState({isLandscape});
+ }
+ };
+
+ userIsLoggedIn = () => {
+ if (
+ this.entities &&
+ this.entities.general &&
+ this.entities.general.credentials &&
+ this.entities.general.credentials.token &&
+ this.entities.general.credentials.url
+ ) {
+ return true;
+ }
+
+ return false;
+ };
+
+ render() {
+ const {init, isLandscape} = this.state;
+
+ if (!init) {
+ return null;
+ }
+
+ const theme = Preferences.THEMES.default;
+
+ const initialRoute = {
+ component: ExtensionPost,
+ title: 'Mattermost',
+ passProps: {
+ authenticated: this.userIsLoggedIn(),
+ entities: this.entities,
+ onClose: this.props.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/share_extension/ios/index.js b/share_extension/ios/index.js
index 46d147f5a..bc748b5de 100644
--- a/share_extension/ios/index.js
+++ b/share_extension/ios/index.js
@@ -2,164 +2,66 @@
// See License.txt for license information.
import React, {PureComponent} from 'react';
+import {NativeModules} from 'react-native';
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 {getTranslations} from 'app/i18n';
-import ExtensionPost from './extension_post';
+import {captureExceptionWithoutState, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry';
+
+import ErrorMessage from './error_message';
+import Extension from './extension';
-const {View: AnimatedView} = Animated;
const ShareExtension = NativeModules.MattermostShare;
-export default class SharedApp extends PureComponent {
+export class SharedApp extends PureComponent {
constructor(props) {
super(props);
- const {height, width} = Dimensions.get('window');
- const isLandscape = width > height;
+ initializeSentry();
this.state = {
- backdropOpacity: new Animated.Value(0),
- containerOpacity: new Animated.Value(0),
- isLandscape,
+ hasError: false,
};
-
- mattermostBucket.readFromFile('entities', Config.AppGroupId).then((value) => {
- this.entities = 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();
+ componentDidCatch(error) {
+ this.setState({hasError: true});
+
+ captureExceptionWithoutState(error, LOGGER_EXTENSION);
}
- onClose = (data) => {
+ close = (data) => {
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});
- }
- };
-
- userIsLoggedIn = () => {
- if (this.entities && this.entities.general && this.entities.general.credentials &&
- this.entities.general.credentials.token && this.entities.general.credentials.url) {
- return true;
- }
-
- return false;
- };
-
render() {
- const {init, isLandscape} = this.state;
-
- if (!init) {
- return null;
+ if (this.state.hasError) {
+ return (
+
+ );
}
- const theme = Preferences.THEMES.default;
- const locale = DeviceInfo.getDeviceLocale().split('-')[0];
-
- const initialRoute = {
- component: ExtensionPost,
- title: 'Mattermost',
- passProps: {
- authenticated: this.userIsLoggedIn(),
- entities: this.entities,
- 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%',
- },
-});
+export default function ShareExtensionProvider() {
+ const locale = DeviceInfo.getDeviceLocale().split('-')[0];
+
+ return (
+
+
+
+ );
+}