[MM-13295] Make channel link clickable for user who's not member of the public channel (#2466)
* Make channel link clickable for user who's not member of the public channel * update per review * update per comment * remove setChannelDisplayName
This commit is contained in:
parent
d69ef1b777
commit
0de0920974
5 changed files with 157 additions and 23 deletions
|
|
@ -4,21 +4,27 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import {getChannelFromChannelName} from './channel_link_utils';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
|
||||
import {getChannelFromChannelName} from './channel_link_utils';
|
||||
|
||||
export default class ChannelLink extends React.PureComponent {
|
||||
static propTypes = {
|
||||
channelName: PropTypes.string.isRequired,
|
||||
channelMentions: PropTypes.object,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
linkStyle: CustomPropTypes.Style,
|
||||
onChannelLinkPress: PropTypes.func,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
channelsByName: PropTypes.object.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
setChannelDisplayName: PropTypes.func.isRequired,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -30,6 +36,10 @@ export default class ChannelLink extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const nextChannel = getChannelFromChannelName(nextProps.channelName, nextProps.channelsByName);
|
||||
if (nextChannel !== prevState.channel) {
|
||||
|
|
@ -39,12 +49,35 @@ export default class ChannelLink extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
this.props.actions.setChannelDisplayName(this.state.channel.display_name);
|
||||
this.props.actions.handleSelectChannel(this.state.channel.id);
|
||||
handlePress = async () => {
|
||||
let {channel} = this.state;
|
||||
|
||||
if (this.props.onChannelLinkPress) {
|
||||
this.props.onChannelLinkPress(this.state.channel);
|
||||
if (!channel.id && channel.display_name) {
|
||||
const {
|
||||
actions,
|
||||
channelName,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
} = this.props;
|
||||
|
||||
const result = await actions.joinChannel(currentUserId, currentTeamId, null, channelName);
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
const joinFailedMessage = {
|
||||
id: t('mobile.join_channel.error'),
|
||||
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.",
|
||||
};
|
||||
alertErrorWithFallback(this.context.intl, result.error || {}, joinFailedMessage, channel.display_name);
|
||||
} else if (result?.data?.channel) {
|
||||
channel = result.data.channel;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.id) {
|
||||
this.props.actions.handleSelectChannel(channel.id);
|
||||
|
||||
if (this.props.onChannelLinkPress) {
|
||||
this.props.onChannelLinkPress(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +88,10 @@ export default class ChannelLink extends React.PureComponent {
|
|||
return <Text style={this.props.textStyle}>{`~${this.props.channelName}`}</Text>;
|
||||
}
|
||||
|
||||
const suffix = this.props.channelName.substring(channel.name.length);
|
||||
let suffix;
|
||||
if (channel.name) {
|
||||
suffix = this.props.channelName.substring(channel.name.length);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={this.props.textStyle}>
|
||||
|
|
|
|||
|
|
@ -5,28 +5,40 @@ import React from 'react';
|
|||
import {shallow} from 'enzyme';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
|
||||
import ChannelLink from './channel_link';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
jest.mock('app/utils/general', () => ({
|
||||
alertErrorWithFallback: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ChannelLink', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const channelsByName = {
|
||||
firstChannel: {id: 'channel_id_1', name: 'firstChannel', display_name: 'First Channel'},
|
||||
secondChannel: {id: 'channel_id_2', name: 'secondChannel', display_name: 'Second Channel'},
|
||||
firstChannel: {id: 'channel_id_1', name: 'firstChannel', display_name: 'First Channel', team_id: 'current_team_id'},
|
||||
secondChannel: {id: 'channel_id_2', name: 'secondChannel', display_name: 'Second Channel', team_id: 'current_team_id'},
|
||||
};
|
||||
const baseProps = {
|
||||
channelName: 'firstChannel',
|
||||
currentTeamId: 'current_team_id',
|
||||
currentUserId: 'current_user_id',
|
||||
linkStyle: {color: '#2389d7'},
|
||||
onChannelLinkPress: jest.fn(),
|
||||
textStyle: {color: '#3d3c40', fontSize: 15, lineHeight: 20},
|
||||
channelsByName,
|
||||
actions: {
|
||||
handleSelectChannel: jest.fn(),
|
||||
setChannelDisplayName: jest.fn(),
|
||||
joinChannel: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelLink {...baseProps}/>
|
||||
<ChannelLink {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
|
@ -59,17 +71,76 @@ describe('ChannelLink', () => {
|
|||
|
||||
test('should call props.actions and onChannelLinkPress on handlePress', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelLink {...baseProps}/>
|
||||
<ChannelLink {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const channel = channelsByName.firstChannel;
|
||||
wrapper.setState({channel});
|
||||
wrapper.instance().handlePress();
|
||||
expect(baseProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.actions.handleSelectChannel).toBeCalledWith(channel.id);
|
||||
expect(baseProps.actions.setChannelDisplayName).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.actions.setChannelDisplayName).toBeCalledWith(channel.display_name);
|
||||
expect(baseProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.onChannelLinkPress).toBeCalledWith(channel);
|
||||
|
||||
expect(baseProps.actions.joinChannel).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should call props.actions.joinChannel on handlePress when user is not member of such channel', async () => {
|
||||
const newChannelName = 'thirdChannel';
|
||||
const thirdChannel = {id: 'channel_id_3', name: 'thirdChannel', display_name: 'thirdChannel', team_id: 'current_team_id'};
|
||||
const error = {message: 'Failed to join a channel'};
|
||||
const joinChannel = jest.fn().
|
||||
mockReturnValueOnce({data: {channel: thirdChannel}}).
|
||||
mockReturnValueOnce({}).
|
||||
mockReturnValueOnce({data: {}}).
|
||||
mockReturnValueOnce({error});
|
||||
const channelMentions = {thirdChannel: {display_name: 'thirdChannel'}};
|
||||
const newChannelsByName = Object.assign({}, channelMentions, channelsByName);
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
channelsByName: newChannelsByName,
|
||||
channelName: newChannelName,
|
||||
actions: {...baseProps.actions, joinChannel},
|
||||
};
|
||||
const intl = {formatMessage};
|
||||
const joinFailedMessage = {
|
||||
id: 'mobile.join_channel.error',
|
||||
defaultMessage: 'We couldn\'t join the channel {displayName}. Please check your connection and try again.',
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ChannelLink {...newProps}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
|
||||
await wrapper.instance().handlePress();
|
||||
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(1);
|
||||
expect(newProps.actions.joinChannel).toBeCalledWith('current_user_id', 'current_team_id', null, newChannelName);
|
||||
expect(alertErrorWithFallback).not.toBeCalled();
|
||||
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
|
||||
expect(newProps.actions.handleSelectChannel).toHaveBeenLastCalledWith(thirdChannel.id);
|
||||
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
|
||||
expect(newProps.onChannelLinkPress).toHaveBeenLastCalledWith(thirdChannel);
|
||||
|
||||
// should have called alertErrorWithFallback on error when joining a channel
|
||||
await wrapper.instance().handlePress();
|
||||
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(2);
|
||||
expect(alertErrorWithFallback).toHaveBeenCalledTimes(1);
|
||||
expect(alertErrorWithFallback).toHaveBeenLastCalledWith(intl, {}, joinFailedMessage, thirdChannel.display_name);
|
||||
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
|
||||
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
|
||||
|
||||
await wrapper.instance().handlePress();
|
||||
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(3);
|
||||
expect(alertErrorWithFallback).toHaveBeenCalledTimes(2);
|
||||
expect(alertErrorWithFallback).toHaveBeenLastCalledWith(intl, {}, joinFailedMessage, thirdChannel.display_name);
|
||||
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
|
||||
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
|
||||
|
||||
await wrapper.instance().handlePress();
|
||||
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(4);
|
||||
expect(alertErrorWithFallback).toHaveBeenCalledTimes(3);
|
||||
expect(alertErrorWithFallback).toHaveBeenLastCalledWith(intl, error, joinFailedMessage, thirdChannel.display_name);
|
||||
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
|
||||
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,16 +3,40 @@
|
|||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {joinChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel';
|
||||
import {handleSelectChannel} from 'app/actions/views/channel';
|
||||
|
||||
import ChannelLink from './channel_link';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
channelsByName: getChannelsNameMapInCurrentTeam(state),
|
||||
function makeGetChannelNamesMap() {
|
||||
return createSelector(
|
||||
getChannelsNameMapInCurrentTeam,
|
||||
(state, props) => props && props.channelMentions,
|
||||
(channelsNameMap, channelMentions) => {
|
||||
if (channelMentions) {
|
||||
return Object.assign({}, channelMentions, channelsNameMap);
|
||||
}
|
||||
|
||||
return channelsNameMap;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getChannelNamesMap = makeGetChannelNamesMap();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
channelsByName: getChannelNamesMap(state, ownProps),
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -20,9 +44,9 @@ function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
actions: bindActionCreators({
|
||||
handleSelectChannel,
|
||||
setChannelDisplayName,
|
||||
joinChannel,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelLink);
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelLink);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default class Markdown extends PureComponent {
|
|||
autolinkedUrlSchemes: PropTypes.array.isRequired,
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
channelMentions: PropTypes.object,
|
||||
imageMetadata: PropTypes.object,
|
||||
isEdited: PropTypes.bool,
|
||||
isReplyPost: PropTypes.bool,
|
||||
|
|
@ -222,6 +223,7 @@ export default class Markdown extends PureComponent {
|
|||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
onChannelLinkPress={this.props.onChannelLinkPress}
|
||||
channelName={channelName}
|
||||
channelMentions={this.props.channelMentions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -379,6 +379,7 @@ export default class PostBody extends PureComponent {
|
|||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
channelMentions={postProps.channel_mentions}
|
||||
imageMetadata={metadata?.images}
|
||||
isEdited={hasBeenEdited}
|
||||
isReplyPost={isReplyPost}
|
||||
|
|
|
|||
Loading…
Reference in a new issue