From 64989a728c74ff2549063968bf34c1a3df4049df Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Wed, 6 Jan 2021 17:21:17 +0100 Subject: [PATCH] =?UTF-8?q?Revert=20"[MM-29225]=20Define=20LSApplicationQu?= =?UTF-8?q?eriesSchemes=20so=20Linking.canO=E2=80=A6=20(#5067)=20(#5091)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "[MM-29225] Define LSApplicationQueriesSchemes so Linking.canOpenURL can work (#5007)" This reverts commit f3baaa6aa3ce0f026a3c5dff6d3740b9660cf17a. * Create and use tryOpenURL * Add missing onError functions * Update app/components/markdown/markdown_link/markdown_link.js Co-authored-by: Elias Nahum Co-authored-by: Elias Nahum (cherry picked from commit 7702c050bf485e45609109aa91840bdd534a589e) Co-authored-by: Miguel Alatzar --- .../client_upgrade_listener.js | 13 ++--- .../markdown/markdown_image/markdown_image.js | 24 ++++++--- .../markdown/markdown_link/markdown_link.js | 38 +++++++------- .../message_attachments/attachment_author.js | 29 +++++++++-- .../message_attachments/attachment_title.js | 31 ++++++++++-- .../post_attachment_opengraph.js | 24 ++++++++- .../post_body_additional_content.js | 19 +++++-- app/mm-redux/actions/integrations.ts | 19 ++++++- app/screens/about/about.js | 38 +++++++++++--- app/screens/client_upgrade/client_upgrade.js | 12 ++--- app/screens/settings/general/settings.js | 50 +++++++++++++------ app/screens/user_profile/user_profile.js | 19 ++++++- app/utils/draft.js | 22 +------- app/utils/supported_server.ts | 20 ++++++-- app/utils/url.js | 11 +++- app/utils/url.test.js | 28 ++++++++++- assets/base/i18n/en.json | 4 ++ ios/Mattermost/Info.plist | 5 -- test/setup.js | 8 ++- 19 files changed, 297 insertions(+), 117 deletions(-) diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js index fbd25d6dc..3b8812456 100644 --- a/app/components/client_upgrade_listener/client_upgrade_listener.js +++ b/app/components/client_upgrade_listener/client_upgrade_listener.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import { Alert, Animated, - Linking, TouchableOpacity, View, } from 'react-native'; @@ -17,6 +16,7 @@ import FormattedText from '@components/formatted_text'; import {DeviceTypes} from '@constants'; import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; import {showModal, dismissModal} from '@actions/navigation'; const {View: AnimatedView} = Animated; @@ -121,11 +121,7 @@ export default class ClientUpgradeListener extends PureComponent { const {downloadLink} = this.props; const {intl} = this.context; - Linking.canOpenURL(downloadLink).then((supported) => { - if (supported) { - return Linking.openURL(downloadLink); - } - + const onError = () => { Alert.alert( intl.formatMessage({ id: 'mobile.client_upgrade.download_error.title', @@ -136,9 +132,8 @@ export default class ClientUpgradeListener extends PureComponent { defaultMessage: 'An error occurred while trying to open the download link.', }), ); - - return false; - }); + }; + tryOpenURL(downloadLink, onError); this.toggleUpgradeMessage(false); }; diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js index e2317234e..35cb93196 100644 --- a/app/components/markdown/markdown_image/markdown_image.js +++ b/app/components/markdown/markdown_image/markdown_image.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {intlShape} from 'react-intl'; import { - Linking, + Alert, Platform, StyleSheet, Text, @@ -24,7 +24,7 @@ import EphemeralStore from '@store/ephemeral_store'; import BottomSheet from '@utils/bottom_sheet'; import {generateId} from '@utils/file'; import {calculateDimensions, getViewPortWidth, isGifTooLarge, openGalleryAtIndex} from '@utils/images'; -import {normalizeProtocol} from '@utils/url'; +import {normalizeProtocol, tryOpenURL} from '@utils/url'; import mattermostManaged from 'app/mattermost_managed'; @@ -123,12 +123,22 @@ export default class MarkdownImage extends ImageViewPort { handleLinkPress = () => { const url = normalizeProtocol(this.props.linkDestination); + const {intl} = this.context; - Linking.canOpenURL(url).then((supported) => { - if (supported) { - Linking.openURL(url); - } - }); + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(url, onError); }; handleLinkLongPress = async () => { diff --git a/app/components/markdown/markdown_link/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js index f34557087..7c5e64da9 100644 --- a/app/components/markdown/markdown_link/markdown_link.js +++ b/app/components/markdown/markdown_link/markdown_link.js @@ -3,10 +3,10 @@ import React, {Children, PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Alert, Linking, Text} from 'react-native'; +import {Alert, Text} from 'react-native'; import Clipboard from '@react-native-community/clipboard'; -import urlParse from 'url-parse'; import {intlShape} from 'react-intl'; +import urlParse from 'url-parse'; import Config from '@assets/config'; import {DeepLinkTypes} from '@constants'; @@ -15,7 +15,7 @@ import {getCurrentServerUrl} from '@init/credentials'; import BottomSheet from '@utils/bottom_sheet'; import {errorBadChannel} from '@utils/draft'; import {preventDoubleTap} from '@utils/tap'; -import {matchDeepLink, normalizeProtocol} from '@utils/url'; +import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url'; import mattermostManaged from 'app/mattermost_managed'; @@ -64,23 +64,21 @@ export default class MarkdownLink extends PureComponent { onPermalinkPress(match.postId, match.teamName); } } else { - Linking.canOpenURL(url).then((supported) => { - if (supported) { - Linking.openURL(url); - } else { - const {formatMessage} = this.context.intl; - Alert.alert( - formatMessage({ - id: 'mobile.server_link.error.title', - defaultMessage: 'Link Error', - }), - formatMessage({ - id: 'mobile.server_link.error.text', - defaultMessage: 'The link could not be found on this server.', - }), - ); - } - }); + const onError = () => { + const {formatMessage} = this.context.intl; + Alert.alert( + formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(url, onError); } }); diff --git a/app/components/message_attachments/attachment_author.js b/app/components/message_attachments/attachment_author.js index c9b408a60..d69f528db 100644 --- a/app/components/message_attachments/attachment_author.js +++ b/app/components/message_attachments/attachment_author.js @@ -2,11 +2,13 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; -import {Linking, Text, View} from 'react-native'; +import {Alert, Text, View} from 'react-native'; import FastImage from 'react-native-fast-image'; +import {intlShape} from 'react-intl'; import PropTypes from 'prop-types'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; export default class AttachmentAuthor extends PureComponent { static propTypes = { @@ -16,10 +18,29 @@ export default class AttachmentAuthor extends PureComponent { theme: PropTypes.object.isRequired, }; + static contextTypes = { + intl: intlShape.isRequired, + }; + openLink = () => { const {link} = this.props; - if (link && Linking.canOpenURL(link)) { - Linking.openURL(link); + const {intl} = this.context; + + if (link) { + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(link, onError); } }; diff --git a/app/components/message_attachments/attachment_title.js b/app/components/message_attachments/attachment_title.js index ab7be0c33..b428de376 100644 --- a/app/components/message_attachments/attachment_title.js +++ b/app/components/message_attachments/attachment_title.js @@ -2,11 +2,13 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; -import {Linking, Text, View} from 'react-native'; +import {Alert, Text, View} from 'react-native'; +import {intlShape} from 'react-intl'; import PropTypes from 'prop-types'; -import {makeStyleSheetFromTheme} from 'app/utils/theme'; -import Markdown from 'app/components/markdown'; +import Markdown from '@components/markdown'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; export default class AttachmentTitle extends PureComponent { static propTypes = { @@ -15,10 +17,29 @@ export default class AttachmentTitle extends PureComponent { value: PropTypes.string, }; + static contextTypes = { + intl: intlShape.isRequired, + }; + openLink = () => { const {link} = this.props; - if (link && Linking.canOpenURL(link)) { - Linking.openURL(link); + const {intl} = this.context; + + if (link) { + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(link, onError); } }; diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.js index ccc2c740e..27b71351b 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.js @@ -3,8 +3,9 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Linking, Text, View} from 'react-native'; +import {Alert, Text, View} from 'react-native'; import FastImage from 'react-native-fast-image'; +import {intlShape} from 'react-intl'; import parseUrl from 'url-parse'; import {TABLET_WIDTH} from '@components/sidebars/drawer_layout'; @@ -14,6 +15,7 @@ import {generateId} from '@utils/file'; import {openGalleryAtIndex, calculateDimensions} from '@utils/images'; import {getNearestPoint} from '@utils/opengraph'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; const MAX_IMAGE_HEIGHT = 150; const VIEWPORT_IMAGE_OFFSET = 93; @@ -31,6 +33,10 @@ export default class PostAttachmentOpenGraph extends PureComponent { theme: PropTypes.object.isRequired, }; + static contextTypes = { + intl: intlShape.isRequired, + }; + constructor(props) { super(props); @@ -153,7 +159,21 @@ export default class PostAttachmentOpenGraph extends PureComponent { }; goToLink = () => { - Linking.openURL(this.props.link); + const {intl} = this.context; + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(this.props.link, onError); }; handlePreviewImage = () => { diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index 0b0e36348..fa69996a6 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import { Alert, Image, - Linking, Platform, StyleSheet, StatusBar, @@ -23,7 +22,7 @@ import CustomPropTypes from '@constants/custom_prop_types'; import EventEmitter from '@mm-redux/utils/event_emitter'; import {generateId} from '@utils/file'; import {calculateDimensions, getViewPortWidth, openGalleryAtIndex} from '@utils/images'; -import {getYouTubeVideoId, isImageLink, isYoutubeLink} from '@utils/url'; +import {getYouTubeVideoId, isImageLink, isYoutubeLink, tryOpenURL} from '@utils/url'; const MAX_YOUTUBE_IMAGE_HEIGHT = 202; const MAX_YOUTUBE_IMAGE_WIDTH = 360; @@ -272,7 +271,21 @@ export default class PostBodyAdditionalContent extends ImageViewPort { startTime, }).catch(this.playYouTubeVideoError); } else { - Linking.openURL(videoLink); + const {intl} = this.context; + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(videoLink, onError); } } }; diff --git a/app/mm-redux/actions/integrations.ts b/app/mm-redux/actions/integrations.ts index 7b6122714..f08ff92ff 100644 --- a/app/mm-redux/actions/integrations.ts +++ b/app/mm-redux/actions/integrations.ts @@ -1,5 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. + +import {Alert} from 'react-native'; + import {IntegrationTypes} from '@mm-redux/action_types'; import {General} from '../constants'; import {Client4} from '@mm-redux/client'; @@ -23,7 +26,7 @@ import {getUserByUsername} from '@mm-redux/actions/users'; import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general'; import * as DraftUtils from '@utils/draft'; import {permalinkBadTeam} from '@utils/general'; -import {getURLAndMatch} from '@utils/url'; +import {getURLAndMatch, tryOpenURL} from '@utils/url'; export function createIncomingHook(hook: IncomingWebhook): ActionFunc { return bindClientFunc({ @@ -454,7 +457,19 @@ export function handleGotoLocation(href: string, intl: any): ActionFunc { break; } } else { - DraftUtils.tryOpenURL(url); + const {formatMessage} = this.context.intl; + const onError = () => Alert.alert( + formatMessage({ + id: 'mobile.server_link.error.title', + defaultMessage: 'Link Error', + }), + formatMessage({ + id: 'mobile.server_link.error.text', + defaultMessage: 'The link could not be found on this server.', + }), + ); + + tryOpenURL(url, onError); } return {data: true}; }; diff --git a/app/screens/about/about.js b/app/screens/about/about.js index 3c43a77dc..46f71a7d9 100644 --- a/app/screens/about/about.js +++ b/app/screens/about/about.js @@ -4,7 +4,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Linking, + Alert, ScrollView, Text, TouchableOpacity, @@ -12,6 +12,7 @@ import { } from 'react-native'; import DeviceInfo from 'react-native-device-info'; import {SafeAreaView} from 'react-native-safe-area-context'; +import {intlShape} from 'react-intl'; import Config from '@assets/config'; import CompassIcon from '@components/compass_icon'; @@ -19,6 +20,7 @@ import FormattedText from '@components/formatted_text'; import StatusBar from '@components/status_bar'; import AboutLinks from '@constants/about_links'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; const MATTERMOST_BUNDLE_IDS = ['com.mattermost.rnbeta', 'com.mattermost.rn']; @@ -29,28 +31,50 @@ export default class About extends PureComponent { theme: PropTypes.object.isRequired, }; + static contextTypes = { + intl: intlShape.isRequired, + }; + + openURL = (url) => { + const {intl} = this.context; + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(url, onError); + }; + handleAboutTeam = () => { - Linking.openURL(Config.AboutTeamURL); + this.openURL(Config.AboutTeamURL); }; handleAboutEnterprise = () => { - Linking.openURL(Config.AboutEnterpriseURL); + this.openURL(Config.AboutEnterpriseURL); }; handlePlatformNotice = () => { - Linking.openURL(Config.PlatformNoticeURL); + this.openURL(Config.PlatformNoticeURL); }; handleMobileNotice = () => { - Linking.openURL(Config.MobileNoticeURL); + this.openURL(Config.MobileNoticeURL); }; handleTermsOfService = () => { - Linking.openURL(AboutLinks.TERMS_OF_SERVICE); + this.openURL(AboutLinks.TERMS_OF_SERVICE); }; handlePrivacyPolicy = () => { - Linking.openURL(AboutLinks.PRIVACY_POLICY); + this.openURL(AboutLinks.PRIVACY_POLICY); } render() { diff --git a/app/screens/client_upgrade/client_upgrade.js b/app/screens/client_upgrade/client_upgrade.js index 0a8b1b056..c9c1c2287 100644 --- a/app/screens/client_upgrade/client_upgrade.js +++ b/app/screens/client_upgrade/client_upgrade.js @@ -7,7 +7,6 @@ import PropTypes from 'prop-types'; import { Alert, Image, - Linking, ScrollView, TouchableOpacity, View, @@ -21,6 +20,7 @@ import StatusBar from '@components/status_bar'; import {UpgradeTypes} from '@constants'; import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; export default class ClientUpgrade extends PureComponent { static propTypes = { @@ -104,11 +104,7 @@ export default class ClientUpgrade extends PureComponent { const {downloadLink} = this.props; const {intl} = this.context; - Linking.canOpenURL(downloadLink).then((supported) => { - if (supported) { - return Linking.openURL(downloadLink); - } - + const onError = () => { Alert.alert( intl.formatMessage({ id: 'mobile.client_upgrade.download_error.title', @@ -119,9 +115,9 @@ export default class ClientUpgrade extends PureComponent { defaultMessage: 'An error occurred while trying to open the download link.', }), ); + }; - return false; - }); + tryOpenURL(downloadLink, onError); }; renderMustUpgrade() { diff --git a/app/screens/settings/general/settings.js b/app/screens/settings/general/settings.js index 70a40d9c5..538a176ab 100644 --- a/app/screens/settings/general/settings.js +++ b/app/screens/settings/general/settings.js @@ -5,7 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape, injectIntl} from 'react-intl'; import { - Linking, + Alert, Platform, ScrollView, View, @@ -21,7 +21,7 @@ import SettingsItem from '@screens/settings/settings_item'; import {t} from '@utils/i18n'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {isValidUrl} from '@utils/url'; +import {isValidUrl, tryOpenURL} from '@utils/url'; class Settings extends PureComponent { static propTypes = { @@ -135,28 +135,48 @@ class Settings extends PureComponent { }); openErrorEmail = preventDoubleTap(() => { - const {config} = this.props; + const {config, intl} = this.props; const recipient = config.SupportEmail; const subject = `Problem with ${config.SiteName} React Native app`; const mailTo = `mailto:${recipient}?subject=${subject}&body=${this.errorEmailBody()}`; - Linking.canOpenURL(mailTo).then((supported) => { - if (supported) { - Linking.openURL(mailTo); - this.props.actions.clearErrors(); - } - }); + const onSuccess = () => { + this.props.actions.clearErrors(); + }; + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.mailTo.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.mailTo.error.text', + defaultMessage: 'Unable to open an email client.', + }), + ); + }; + + tryOpenURL(mailTo, onError, onSuccess); }); openHelp = preventDoubleTap(() => { - const {config} = this.props; + const {config, intl} = this.props; const link = config.HelpLink ? config.HelpLink.toLowerCase() : ''; - Linking.canOpenURL(link).then((supported) => { - if (supported) { - Linking.openURL(link); - } - }); + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(link, onError); }); render() { diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js index 70058b902..93ee74145 100644 --- a/app/screens/user_profile/user_profile.js +++ b/app/screens/user_profile/user_profile.js @@ -4,10 +4,10 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { + Alert, ScrollView, Text, View, - Linking, } from 'react-native'; import {intlShape} from 'react-intl'; import {Navigation} from 'react-native-navigation'; @@ -30,6 +30,7 @@ import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; import {alertErrorWithFallback} from '@utils/general'; import {t} from '@utils/i18n'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {tryOpenURL} from '@utils/url'; import {isGuest} from '@utils/users'; import UserProfileRow from './user_profile_row'; @@ -229,11 +230,25 @@ export default class UserProfile extends PureComponent { handleLinkPress = (link) => { const username = this.props.user.username; const email = this.props.user.email; + const {intl} = this.context; return () => { let hydrated = link.replace(/{email}/, email); hydrated = hydrated.replace(/{username}/, username); - Linking.openURL(hydrated); + + const onError = () => { + Alert.alert( + intl.formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + intl.formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + tryOpenURL(hydrated, onError); }; }; diff --git a/app/utils/draft.js b/app/utils/draft.js index 9d4053904..53255a8b6 100644 --- a/app/utils/draft.js +++ b/app/utils/draft.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Alert, Linking} from 'react-native'; +import {Alert} from 'react-native'; import {AT_MENTION_REGEX_GLOBAL, CODE_REGEX} from '@constants/autocomplete'; import {NOTIFY_ALL_MEMBERS} from '@constants/view'; @@ -81,26 +81,6 @@ export function alertSendToGroups(formatMessage, notifyAllMessage, accept, cance ); } -export function tryOpenURL(url) { - Linking.canOpenURL(url).then((supported) => { - if (supported) { - Linking.openURL(url); - } else { - const {formatMessage} = this.context.intl; - Alert.alert( - formatMessage({ - id: 'mobile.server_link.error.title', - defaultMessage: 'Link Error', - }), - formatMessage({ - id: 'mobile.server_link.error.text', - defaultMessage: 'The link could not be found on this server.', - }), - ); - } - }); -} - export function errorBadChannel(intl) { const message = { id: t('mobile.server_link.unreachable_channel.error'), diff --git a/app/utils/supported_server.ts b/app/utils/supported_server.ts index cc9788fdb..7225d27bb 100644 --- a/app/utils/supported_server.ts +++ b/app/utils/supported_server.ts @@ -1,8 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Alert, Linking, AlertButton} from 'react-native'; +import {Alert, AlertButton} from 'react-native'; import {ViewTypes} from '@constants'; +import {tryOpenURL} from '@utils/url'; interface FormatObjectType { id: string; @@ -36,9 +37,20 @@ function unsupportedServerAdminAlert(formatMessage: FormatMessageType) { style: 'cancel', onPress: () => { const url = 'https://mattermost.com/blog/support-for-esr-5-9-has-ended/'; - if (Linking.canOpenURL(url)) { - Linking.openURL(url); - } + const onError = () => { + Alert.alert( + formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; + + tryOpenURL(url, onError); }, }; const buttons: AlertButton[] = [cancel, learnMore]; diff --git a/app/utils/url.js b/app/utils/url.js index d002ad9f5..e4b1618ec 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -1,13 +1,16 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Linking} from 'react-native'; + import {latinise} from './latinise.js'; import {escapeRegex} from './markdown'; import {Files} from '@mm-redux/constants'; import {getCurrentServerUrl} from '@init/credentials'; -import {DeepLinkTypes} from 'app/constants'; +import {DeepLinkTypes} from '@constants'; +import {emptyFunction} from '@utils/general'; const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/; @@ -180,3 +183,9 @@ export async function getURLAndMatch(href, serverURL, siteURL) { return {url, match}; } + +export function tryOpenURL(url, onError = emptyFunction, onSuccess = emptyFunction) { + Linking.openURL(url). + then(onSuccess). + catch(onError); +} diff --git a/app/utils/url.test.js b/app/utils/url.test.js index 38bcffba4..1306c49f9 100644 --- a/app/utils/url.test.js +++ b/app/utils/url.test.js @@ -1,7 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import * as UrlUtils from 'app/utils/url'; +import {Linking} from 'react-native'; + +import * as UrlUtils from '@utils/url'; /* eslint-disable max-nested-callbacks */ @@ -131,4 +133,28 @@ describe('UrlUtils', () => { }); } }); + + describe('tryOpenUrl', () => { + const url = 'https://some.url.com'; + + it('should call onSuccess when Linking.openURL succeeds', async () => { + Linking.openURL.mockResolvedValueOnce(); + const onError = jest.fn(); + const onSuccess = jest.fn(); + + await UrlUtils.tryOpenURL(url, onError, onSuccess); + expect(onError).not.toHaveBeenCalled(); + expect(onSuccess).toHaveBeenCalledTimes(1); + }); + + it('should call onError when Linking.openURL fails', async () => { + Linking.openURL.mockRejectedValueOnce(); + const onError = jest.fn(); + const onSuccess = jest.fn(); + + await UrlUtils.tryOpenURL(url, onError, onSuccess); + expect(onError).toHaveBeenCalledTimes(1); + expect(onSuccess).not.toHaveBeenCalled(); + }); + }); }); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 35c4de889..9bbc5c85c 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -291,12 +291,16 @@ "mobile.ios.photos_permission_denied_description": "Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.", "mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos", "mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.", + "mobile.link.error.text": "Unable to open the link.", + "mobile.link.error.title": "Error", "mobile.loading_channels": "Loading Channels...", "mobile.loading_members": "Loading Members...", "mobile.loading_options": "Loading Options...", "mobile.loading_posts": "Loading messages...", "mobile.login_options.choose_title": "Choose your login method", "mobile.long_post_title": "{channelName} - Post", + "mobile.mailTo.error.text": "Unable to open an email client.", + "mobile.mailTo.error.title": "Error", "mobile.managed.blocked_by": "Blocked by {vendor}", "mobile.managed.exit": "Exit", "mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.", diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 63d6f02ae..b356cac45 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -129,10 +129,5 @@ UIViewControllerBasedStatusBarAppearance - LSApplicationQueriesSchemes - - https - http - diff --git a/test/setup.js b/test/setup.js index 2c53ff475..4a4ffb3c3 100644 --- a/test/setup.js +++ b/test/setup.js @@ -30,6 +30,7 @@ jest.doMock('react-native', () => { Alert: RNAlert, InteractionManager: RNInteractionManager, NativeModules: RNNativeModules, + Linking: RNLinking, } = ReactNative; const Alert = { @@ -96,6 +97,11 @@ jest.doMock('react-native', () => { }, }; + const Linking = { + ...RNLinking, + openURL: jest.fn(), + }; + return Object.setPrototypeOf({ Platform: { ...Platform, @@ -110,6 +116,7 @@ jest.doMock('react-native', () => { Alert, InteractionManager, NativeModules, + Linking, }, ReactNative); }); @@ -187,7 +194,6 @@ jest.mock('react-native-cookies', () => ({ addEventListener: jest.fn(), removeEventListener: jest.fn(), openURL: jest.fn(), - canOpenURL: jest.fn(), getInitialURL: jest.fn(), clearAll: jest.fn(), get: () => Promise.resolve(({