diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index 423839941..2a62d4bcf 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -8,6 +8,7 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
+ getChannelByNameAndTeamName,
markChannelAsRead,
selectChannel,
leaveChannel as serviceLeaveChannel,
@@ -373,6 +374,20 @@ export function handleSelectChannel(channelId) {
};
}
+export function handleSelectChannelByName(channelName, teamName) {
+ return async (dispatch, getState) => {
+ const state = getState();
+ const {teams: currentTeams, currentTeamId} = state.entities.teams;
+ const currentTeamName = currentTeams[currentTeamId].name;
+ const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
+ const currentChannelId = getCurrentChannelId(state);
+ if (channel && currentChannelId !== channel.id) {
+ dispatch(setChannelDisplayName(channel.display_name));
+ dispatch(handleSelectChannel(channel.id));
+ }
+ };
+}
+
export function handlePostDraftChanged(channelId, draft) {
return async (dispatch, getState) => {
dispatch({
diff --git a/app/actions/views/root.test.js b/app/actions/views/root.test.js
new file mode 100644
index 000000000..bb1adcee7
--- /dev/null
+++ b/app/actions/views/root.test.js
@@ -0,0 +1,23 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+
+import {setDeepLinkURL} from './root';
+
+const mockStore = configureStore([thunk]);
+
+describe('Actions.Views.Root', () => {
+ const store = mockStore();
+
+ test('should set deep link URL', async () => {
+ const url = 'https://test-url.com/team-name/pl/pl-id';
+ const action = {
+ type: 'SET_DEEP_LINK_URL',
+ url,
+ };
+ await store.dispatch(setDeepLinkURL(url));
+ expect(store.getActions()).toEqual([action]);
+ });
+});
diff --git a/app/components/markdown/markdown_link/index.js b/app/components/markdown/markdown_link/index.js
index e9d171ec8..b280f3645 100644
--- a/app/components/markdown/markdown_link/index.js
+++ b/app/components/markdown/markdown_link/index.js
@@ -1,9 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
+import {handleSelectChannelByName} from 'app/actions/views/channel';
import MarkdownLink from './markdown_link';
@@ -14,4 +16,12 @@ function mapStateToProps(state) {
};
}
-export default connect(mapStateToProps)(MarkdownLink);
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ handleSelectChannelByName,
+ }, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MarkdownLink);
diff --git a/app/components/markdown/markdown_link/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js
index 8aae75fa1..ed76eaf4f 100644
--- a/app/components/markdown/markdown_link/markdown_link.js
+++ b/app/components/markdown/markdown_link/markdown_link.js
@@ -8,15 +8,19 @@ import urlParse from 'url-parse';
import {intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
+import {DeepLinkTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import {preventDoubleTap} from 'app/utils/tap';
-import {matchPermalink, normalizeProtocol} from 'app/utils/url';
+import {matchDeepLink, normalizeProtocol} from 'app/utils/url';
import Config from 'assets/config';
export default class MarkdownLink extends PureComponent {
static propTypes = {
+ actions: PropTypes.shape({
+ handleSelectChannelByName: PropTypes.func.isRequired,
+ }).isRequired,
children: CustomPropTypes.Children.isRequired,
href: PropTypes.string.isRequired,
onPermalinkPress: PropTypes.func,
@@ -40,12 +44,13 @@ export default class MarkdownLink extends PureComponent {
return;
}
- const match = matchPermalink(url, serverURL) || matchPermalink(url, siteURL) || matchPermalink(url, '');
-
+ const match = matchDeepLink(url, serverURL, siteURL);
if (match) {
- const teamName = match[1];
- const postId = match[2];
- onPermalinkPress(postId, teamName);
+ if (match.type === DeepLinkTypes.CHANNEL) {
+ this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);
+ } else if (match.type === DeepLinkTypes.PERMALINK) {
+ onPermalinkPress(match.postId, match.teamName);
+ }
} else {
Linking.canOpenURL(url).then((supported) => {
if (supported) {
diff --git a/app/components/post_list/__snapshots__/post_list.test.js.snap b/app/components/post_list/__snapshots__/post_list.test.js.snap
new file mode 100644
index 000000000..48f9e4807
--- /dev/null
+++ b/app/components/post_list/__snapshots__/post_list.test.js.snap
@@ -0,0 +1,145 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PostList setting channel deep link 1`] = `
+
+`;
+
+exports[`PostList setting permalink deep link 1`] = `
+
+`;
+
+exports[`PostList should match snapshot 1`] = `
+
+`;
diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js
index 1da2e0fec..51c4e75e2 100644
--- a/app/components/post_list/index.js
+++ b/app/components/post_list/index.js
@@ -8,7 +8,7 @@ import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel';
+import {handleSelectChannelByName, loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel';
import {setDeepLinkURL} from 'app/actions/views/root';
import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
@@ -37,6 +37,7 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
+ handleSelectChannelByName,
loadChannelsByTeamName,
refreshChannelWithRetry,
selectFocusedPostId,
diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js
new file mode 100644
index 000000000..1e12cab13
--- /dev/null
+++ b/app/components/post_list/post_list.test.js
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import PostList from './post_list.ios.js';
+import Preferences from 'mattermost-redux/constants/preferences';
+
+jest.useFakeTimers();
+
+describe('PostList', () => {
+ const serverURL = 'https://server-url.fake';
+ const baseProps = {
+ actions: {
+ handleSelectChannelByName: jest.fn(),
+ loadChannelsByTeamName: jest.fn(),
+ refreshChannelWithRetry: jest.fn(),
+ selectFocusedPostId: jest.fn(),
+ setDeepLinkURL: jest.fn(),
+ },
+ deepLinkURL: '',
+ navigator: {
+ showModal: jest.fn(),
+ },
+ postIds: ['post-id-1', 'post-id-2'],
+ serverURL,
+ siteURL: 'https://site-url.fake',
+ theme: Preferences.THEMES.default,
+ };
+
+ const deepLinks = {
+ permalink: serverURL + '/team-name/pl/pl-id',
+ channel: serverURL + '/team-name/channels/channel-name',
+ };
+
+ const wrapper = shallow(
+
+ );
+
+ test('should match snapshot', () => {
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('setting permalink deep link', () => {
+ wrapper.setProps({deepLinkURL: deepLinks.permalink});
+ expect(baseProps.actions.setDeepLinkURL).toHaveBeenCalled();
+ expect(baseProps.actions.selectFocusedPostId).toHaveBeenCalled();
+ expect(baseProps.navigator.showModal).toHaveBeenCalled();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('setting channel deep link', () => {
+ wrapper.setProps({deepLinkURL: deepLinks.channel});
+ expect(baseProps.actions.setDeepLinkURL).toHaveBeenCalled();
+ expect(baseProps.actions.handleSelectChannelByName).toHaveBeenCalled();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/post_list/post_list_base.js b/app/components/post_list/post_list_base.js
index 3a26237e5..aadbc2087 100644
--- a/app/components/post_list/post_list_base.js
+++ b/app/components/post_list/post_list_base.js
@@ -5,10 +5,11 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import Post from 'app/components/post';
+import {DeepLinkTypes} from 'app/constants';
import {START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import mattermostManaged from 'app/mattermost_managed';
import {changeOpacity} from 'app/utils/theme';
-import {matchPermalink} from 'app/utils/url';
+import {matchDeepLink} from 'app/utils/url';
import DateHeader from './date_header';
import {isDateLine} from './date_header/utils';
@@ -17,6 +18,7 @@ import NewMessagesDivider from './new_messages_divider';
export default class PostListBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
+ handleSelectChannelByName: PropTypes.func.isRequired,
loadChannelsByTeamName: PropTypes.func.isRequired,
refreshChannelWithRetry: PropTypes.func.isRequired,
selectFocusedPostId: PropTypes.func.isRequired,
@@ -82,12 +84,13 @@ export default class PostListBase extends PureComponent {
handleDeepLink = (url) => {
const {serverURL, siteURL} = this.props;
- const match = matchPermalink(url, serverURL) || matchPermalink(url, siteURL);
-
+ const match = matchDeepLink(url, serverURL, siteURL);
if (match) {
- const teamName = match[1];
- const postId = match[2];
- this.handlePermalinkPress(postId, teamName);
+ 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);
+ }
}
};
diff --git a/app/constants/deep_linking.js b/app/constants/deep_linking.js
new file mode 100644
index 000000000..f3ac632db
--- /dev/null
+++ b/app/constants/deep_linking.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export default {
+ CHANNEL: 'channel',
+ PERMALINK: 'permalink',
+ OTHER: 'other',
+};
diff --git a/app/constants/index.js b/app/constants/index.js
index 575e5f161..9182d6788 100644
--- a/app/constants/index.js
+++ b/app/constants/index.js
@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import DeepLinkTypes from './deep_linking';
import DeviceTypes from './device';
import ListTypes from './list';
import NavigationTypes from './navigation';
@@ -8,6 +9,7 @@ import PermissionTypes from './permissions';
import ViewTypes, {UpgradeTypes} from './view';
export {
+ DeepLinkTypes,
DeviceTypes,
ListTypes,
NavigationTypes,
diff --git a/app/utils/url.js b/app/utils/url.js
index 4881437db..ef006979d 100644
--- a/app/utils/url.js
+++ b/app/utils/url.js
@@ -6,6 +6,8 @@ import {escapeRegex} from './markdown';
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\/))([^#&?]*)/;
export function isValidUrl(url = '') {
@@ -96,8 +98,20 @@ export function getScheme(url) {
return match && match[1];
}
-export function matchPermalink(link, rootURL) {
- return new RegExp('^' + escapeRegex(rootURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)').exec(link);
+export function matchDeepLink(url, serverURL, siteURL) {
+ const linkRoot = `(?:${escapeRegex(serverURL)}|${escapeRegex(siteURL)})?`;
+
+ 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);
+ if (match) {
+ return {type: DeepLinkTypes.PERMALINK, teamName: match[1], postId: match[2]};
+ }
+
+ return null;
}
export function getYouTubeVideoId(link) {