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 ( + + + + ); +}