MM-21369 Require server/site URL for deep links (#3770)

**Old (only worked without serverURL or siteURL)**

Beta
* `mattermost-beta://<teamname>/channels/<channelname>`
* `mattermost-beta://<teamname>/pl/<permalinkID>`

Release
* `mattermost-mobile://<teamname>/channels/<channelname>`
* `mattermost-mobile://<teamname>/pl/<permalinkID>`

**New working deep link patterns**

Beta
* `mattermost-beta://<server-or-site-URL><teamname>/channels/<channelname>`
* `mattermost-beta://<server-or-site-URL><teamname>/pl/<permalinkID>`
* `mattermost-beta://https://<server-or-site-URL><teamname>/channels/<channelname>`
* `mattermost-beta://https://<server-or-site-URL><teamname>/pl/<permalinkID>`

Note: Transport protocol (http, https, etc.) is optional.

Release
* `mattermost-mobile://<server-or-site-URL><teamname>/channels/<channelname>`
* `mattermost-mobile://<server-or-site-URL><teamname>/pl/<permalinkID>`
* `mattermost-mobile://https://<server-or-site-URL><teamname>/channels/<channelname>`
* `mattermost-mobile://https://<server-or-site-URL><teamname>/pl/<permalinkID>`

Note: Transport protocol (http, https, etc.) is optional.
This commit is contained in:
Amit Uttam
2020-01-13 16:28:14 -03:00
committed by GitHub
parent 13866bdfce
commit a5330bc08f
7 changed files with 70 additions and 18 deletions

View File

@@ -3,7 +3,7 @@
import React, {Children, PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Clipboard, Linking, Text} from 'react-native';
import {Alert, Clipboard, Linking, Text} from 'react-native';
import urlParse from 'url-parse';
import {intlShape} from 'react-intl';
@@ -52,7 +52,8 @@ export default class MarkdownLink extends PureComponent {
serverUrl = await getCurrentServerUrl();
}
const match = matchDeepLink(url, serverUrl, siteURL);
const match = matchDeepLink(url, serverURL, siteURL);
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);
@@ -63,6 +64,18 @@ export default class MarkdownLink extends PureComponent {
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.',
}),
);
}
});
}

View File

@@ -3,7 +3,8 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, RefreshControl, StyleSheet} from 'react-native';
import {Alert, FlatList, RefreshControl, StyleSheet} from 'react-native';
import {intlShape} from 'react-intl';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import * as PostListUtils from 'mattermost-redux/utils/post_list';
@@ -78,6 +79,10 @@ export default class PostList extends PureComponent {
postIds: [],
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
@@ -163,12 +168,25 @@ export default class PostList extends PureComponent {
const {serverURL, siteURL} = this.props;
const match = matchDeepLink(url, serverURL, siteURL);
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);
} else if (match.type === DeepLinkTypes.PERMALINK) {
this.handlePermalinkPress(match.postId, match.teamName);
}
} 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.',
}),
);
}
};

View File

@@ -10,9 +10,12 @@ import * as NavigationActions from 'app/actions/navigation';
import PostList from './post_list';
jest.useFakeTimers();
jest.mock('react-intl');
describe('PostList', () => {
const serverURL = 'https://server-url.fake';
const deeplinkRoot = 'mattermost-beta://server-url.fake';
const baseProps = {
actions: {
handleSelectChannelByName: jest.fn(),
@@ -31,8 +34,8 @@ describe('PostList', () => {
};
const deepLinks = {
permalink: serverURL + '/team-name/pl/pl-id',
channel: serverURL + '/team-name/channels/channel-name',
permalink: deeplinkRoot + '/team-name/pl/pl-id',
channel: deeplinkRoot + '/team-name/channels/channel-name',
};
test('should match snapshot', () => {

View File

@@ -9,7 +9,6 @@ import {Files} from 'mattermost-redux/constants';
import {DeepLinkTypes} from 'app/constants';
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
const APP_SCHEME = 'mattermost-beta';
export function isValidUrl(url = '') {
const regex = /^https?:\/\//i;
@@ -104,15 +103,18 @@ export function matchDeepLink(url, serverURL, siteURL) {
return null;
}
const linkRoot = `(?:${escapeRegex(APP_SCHEME)}:\\/|${escapeRegex(serverURL)}|${escapeRegex(siteURL)})?`;
const serverURLWithoutProtocol = removeProtocol(serverURL);
const siteURLWithoutProtocol = removeProtocol(siteURL);
let match = new RegExp('^' + linkRoot + '\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url);
const linkRoot = `(?:${escapeRegex(serverURLWithoutProtocol || siteURLWithoutProtocol)})`;
let match = new RegExp(linkRoot + '\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url);
if (match) {
return {type: DeepLinkTypes.CHANNEL, teamName: match[1], channelName: match[2]};
}
match = new RegExp('^' + linkRoot + '\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url);
match = new RegExp(linkRoot + '\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url);
if (match) {
return {type: DeepLinkTypes.PERMALINK, teamName: match[1], postId: match[2]};
}

View File

@@ -87,17 +87,39 @@ describe('UrlUtils', () => {
});
});
describe('removeProtocol', () => {
const tests = [
{name: 'should return url without http protocol prefix', url: 'http://localhost:8065', expected: 'localhost:8065'},
{name: 'should return url without https protocol prefix', url: 'https://localhost:8065', expected: 'localhost:8065'},
{name: 'should return null', url: '', expected: ''},
{name: 'should return url without arbitrary protocol prefix', url: 'udp://localhost:8065', expected: 'localhost:8065'},
];
for (const test of tests) {
const {name, url, expected} = test;
it(name, () => {
expect(UrlUtils.removeProtocol(url)).toEqual(expected);
});
}
});
describe('matchDeepLink', () => {
const SITE_URL = 'http://localhost:8065';
const SERVER_URL = 'http://localhost:8065';
const DEEPLINK_URL_ROOT = 'mattermost-beta://localhost:8065';
const tests = [
{name: 'should return null if all inputs are empty', input: {url: '', serverURL: '', siteURL: ''}, expected: null},
{name: 'should return null if any of the input is null', input: {url: '', serverURL: '', siteURL: null}, expected: null},
{name: 'should return null if any of the input is null', input: {url: '', serverURL: null, siteURL: ''}, expected: null},
{name: 'should return null if any of the input is null', input: {url: null, serverURL: '', siteURL: ''}, expected: null},
{name: 'should return null for not supported link', input: {url: 'https://mattermost.com', serverURL: SERVER_URL, siteURL: SITE_URL}, expected: null},
{name: 'should return null for not supported link', input: {url: 'https://otherserver.com', serverURL: SERVER_URL, siteURL: SITE_URL}, expected: null},
{name: 'should return null despite url subset match', input: {url: 'http://myserver.com', serverURL: 'http://myserver.co'}, expected: null},
{name: 'should match channel link', input: {url: SITE_URL + '/ad-1/channels/town-square', serverURL: SERVER_URL, siteURL: SITE_URL}, expected: {channelName: 'town-square', teamName: 'ad-1', type: 'channel'}},
{name: 'should match permalink', input: {url: SITE_URL + '/ad-1/pl/qe93kkfd7783iqwuwfcwcxbsgy', serverURL: SERVER_URL, siteURL: SITE_URL}, expected: {postId: 'qe93kkfd7783iqwuwfcwcxbsgy', teamName: 'ad-1', type: 'permalink'}},
{name: 'should match channel link with deeplink prefix', input: {url: DEEPLINK_URL_ROOT + '/ad-1/channels/town-square', serverURL: SERVER_URL, siteURL: SITE_URL}, expected: {channelName: 'town-square', teamName: 'ad-1', type: 'channel'}},
{name: 'should match permalink with depplink prefix', input: {url: DEEPLINK_URL_ROOT + '/ad-1/pl/qe93kkfd7783iqwuwfcwcxbsgy', serverURL: SERVER_URL, siteURL: SITE_URL}, expected: {postId: 'qe93kkfd7783iqwuwfcwcxbsgy', teamName: 'ad-1', type: 'permalink'}},
];
for (const test of tests) {

View File

@@ -443,6 +443,8 @@
"mobile.select_team.guest_cant_join_team": "Your guest account has no teams or channels assigned. Please contact an administrator.",
"mobile.select_team.join_open": "Open teams you can join",
"mobile.select_team.no_teams": "There are no available teams for you to join.",
"mobile.server_link.error.text": "The link could not be found on this server.",
"mobile.server_link.error.title": "Link Error",
"mobile.server_upgrade.button": "OK",
"mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n",
"mobile.server_upgrade.title": "Server upgrade required",

View File

@@ -167,14 +167,6 @@ lane :configure do
# Save the config.json file
save_config_json('../dist/assets/config.json', json)
# Set deep link prefix for URL lookups
app_scheme = ENV['APP_SCHEME'] || 'mattermost-beta'
find_replace_string(
path_to_file: './app/utils/url.js',
old_string: "APP_SCHEME = 'mattermost-beta'",
new_string: "APP_SCHEME = '#{app_scheme}'",
)
configured = true
end