Deep linking based on channel url; Fix merge conflicts (#2542)

Code cleanup
This commit is contained in:
Chris Duarte 2019-02-14 10:29:57 -08:00 committed by Harrison Healey
parent 7eb434b256
commit ffe6b9e2ef
11 changed files with 301 additions and 16 deletions

View file

@ -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({

View file

@ -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]);
});
});

View file

@ -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);

View file

@ -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) {

View file

@ -0,0 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostList setting channel deep link 1`] = `
<FlatList
ListFooterComponent={[Function]}
contentContainerStyle={
Object {
"paddingTop": 5,
}
}
data={
Array [
"post-id-1",
"post-id-2",
]
}
disableVirtualization={false}
extraData={
Array [
undefined,
undefined,
undefined,
]
}
horizontal={false}
initialNumToRender={15}
inverted={true}
keyExtractor={[Function]}
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
"minIndexForVisible": 0,
}
}
maxToRenderPerBatch={16}
numColumns={1}
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshing={false}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
updateCellsBatchingPeriod={50}
windowSize={21}
/>
`;
exports[`PostList setting permalink deep link 1`] = `
<FlatList
ListFooterComponent={[Function]}
contentContainerStyle={
Object {
"paddingTop": 5,
}
}
data={
Array [
"post-id-1",
"post-id-2",
]
}
disableVirtualization={false}
extraData={
Array [
undefined,
undefined,
undefined,
]
}
horizontal={false}
initialNumToRender={15}
inverted={true}
keyExtractor={[Function]}
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
"minIndexForVisible": 0,
}
}
maxToRenderPerBatch={16}
numColumns={1}
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshing={false}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
updateCellsBatchingPeriod={50}
windowSize={21}
/>
`;
exports[`PostList should match snapshot 1`] = `
<FlatList
ListFooterComponent={[Function]}
contentContainerStyle={
Object {
"paddingTop": 5,
}
}
data={
Array [
"post-id-1",
"post-id-2",
]
}
disableVirtualization={false}
extraData={
Array [
undefined,
undefined,
undefined,
]
}
horizontal={false}
initialNumToRender={15}
inverted={true}
keyExtractor={[Function]}
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
"minIndexForVisible": 0,
}
}
maxToRenderPerBatch={16}
numColumns={1}
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
onLayout={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
refreshing={false}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={60}
updateCellsBatchingPeriod={50}
windowSize={21}
/>
`;

View file

@ -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,

View file

@ -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(
<PostList {...baseProps}/>
);
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();
});
});

View file

@ -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);
}
}
};

View file

@ -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',
};

View file

@ -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,

View file

@ -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) {