Deep linking based on channel url; Fix merge conflicts (#2542)
Code cleanup
This commit is contained in:
parent
7eb434b256
commit
ffe6b9e2ef
11 changed files with 301 additions and 16 deletions
|
|
@ -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({
|
||||
|
|
|
|||
23
app/actions/views/root.test.js
Normal file
23
app/actions/views/root.test.js
Normal 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]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
145
app/components/post_list/__snapshots__/post_list.test.js.snap
Normal file
145
app/components/post_list/__snapshots__/post_list.test.js.snap
Normal 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}
|
||||
/>
|
||||
`;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
59
app/components/post_list/post_list.test.js
Normal file
59
app/components/post_list/post_list.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
8
app/constants/deep_linking.js
Normal file
8
app/constants/deep_linking.js
Normal 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',
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue