Added "Join prompt" when sys admin clicks on perma links of private channels (#4988)
* Added join prompt when sys admin clicks on private channel perma links * Moved isChannelMember to utils * Update app/actions/views/permalink.ts Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * Added error messages * Added error messages * Moved checks to screen components * Removed unused Alert import * Removed unused translations and showing channel 'display' name * fixed eslint error * Moved private channel check logic from component to handleSelectChannelByName to avoid issues while pressing on url from different team * Detox test WIP * Test link fixes * Added e2e test * Hiding <Loading component on RN Alert as detox is unable to press on the Join button * Updated message * Added team membership check and auto-joining teams before viewing posts * Fixed testcases * Leaves newly joined team on any error while joining the channel * Leaves the team if newly joined if cancelled the prompt * Moved code to tapping_channel_url_link_joins_channel and refactored code * Removed unwanted optional chaining operator Co-authored-by: anurag shivarathri <anuragindianbraves@gmail.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
43645d781e
commit
3c51b39bf4
14 changed files with 368 additions and 64 deletions
|
|
@ -8,13 +8,13 @@ import {ViewTypes} from 'app/constants';
|
|||
import {ChannelTypes, RoleTypes, GroupTypes} from '@mm-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByNameAndTeamName,
|
||||
getChannelByName,
|
||||
joinChannel,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
} from '@mm-redux/actions/channels';
|
||||
import {savePreferences} from '@mm-redux/actions/preferences';
|
||||
import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {selectTeam} from '@mm-redux/actions/teams';
|
||||
import {addUserToTeam, getTeamByName, removeUserFromTeam, selectTeam} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
|
|
@ -27,7 +27,7 @@ import {
|
|||
isManuallyUnread,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getTeamByName, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
|
@ -37,7 +37,7 @@ import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@
|
|||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import {getChannelReachable} from '@selectors/channel';
|
||||
import telemetry from '@telemetry';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue} from '@utils/channels';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue, privateChannelJoinPrompt} from '@utils/channels';
|
||||
import {isPendingPost} from '@utils/general';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
|
@ -48,7 +48,7 @@ export function loadChannelsByTeamName(teamName, errorHandler) {
|
|||
const {currentTeamId} = state.entities.teams;
|
||||
|
||||
if (teamName) {
|
||||
const team = getTeamByName(state, teamName);
|
||||
const team = selectTeamByName(state, teamName);
|
||||
|
||||
if (!team && errorHandler) {
|
||||
errorHandler();
|
||||
|
|
@ -218,16 +218,35 @@ export function handleSelectChannel(channelId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
export function handleSelectChannelByName(channelName, teamName, errorHandler, intl) {
|
||||
return async (dispatch, getState) => {
|
||||
let state = getState();
|
||||
const {teams: currentTeams, currentTeamId} = state.entities.teams;
|
||||
const currentTeam = currentTeams[currentTeamId];
|
||||
const currentTeamName = currentTeam?.name;
|
||||
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName, true));
|
||||
const {error, data: channel} = response;
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
const {error: teamError, data: team} = await dispatch(getTeamByName(teamName || currentTeamName));
|
||||
|
||||
// Fallback to API response error, if any.
|
||||
if (teamError) {
|
||||
if (errorHandler) {
|
||||
errorHandler();
|
||||
}
|
||||
return {error: teamError};
|
||||
}
|
||||
|
||||
// Join team if not a member already
|
||||
const myTeamMemberships = getTeamMemberships(state);
|
||||
let joinedNewTeam = false;
|
||||
if (!myTeamMemberships[team.id]) {
|
||||
await dispatch(addUserToTeam(team.id, currentUserId));
|
||||
joinedNewTeam = true;
|
||||
}
|
||||
|
||||
const {error: channelError, data: channel} = await dispatch(getChannelByName(team.id, channelName));
|
||||
|
||||
state = getState();
|
||||
const reachable = getChannelReachable(state, channelName, teamName);
|
||||
|
||||
|
|
@ -236,27 +255,39 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
|||
}
|
||||
|
||||
// Fallback to API response error, if any.
|
||||
if (error) {
|
||||
return {error};
|
||||
if (channelError) {
|
||||
return {error: channelError};
|
||||
}
|
||||
|
||||
// Join Channel if not a member already
|
||||
if (channel && currentChannelId !== channel.id) {
|
||||
const myChannelMemberships = getMyChannelMemberships(state);
|
||||
if (!myChannelMemberships[channel.id]) {
|
||||
if (channel.type === General.PRIVATE_CHANNEL) {
|
||||
const {join} = await privateChannelJoinPrompt(channel, intl);
|
||||
if (!join) {
|
||||
if (joinedNewTeam) {
|
||||
await dispatch(removeUserFromTeam(team.id, currentUserId));
|
||||
}
|
||||
return {data: true};
|
||||
}
|
||||
}
|
||||
console.log('joining channel', channel?.display_name, channel.id); //eslint-disable-line
|
||||
const result = await dispatch(joinChannel(currentUserId, '', channel.id));
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
if (joinedNewTeam) {
|
||||
await dispatch(removeUserFromTeam(team.id, currentUserId));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (teamName && teamName !== currentTeamName) {
|
||||
const team = getTeamByName(state, teamName);
|
||||
dispatch(selectTeam(team));
|
||||
}
|
||||
|
||||
if (channel && currentChannelId !== channel.id) {
|
||||
if (channel.type === General.OPEN_CHANNEL) {
|
||||
const myMemberships = getMyChannelMemberships(state);
|
||||
if (!myMemberships[channel.id]) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
console.log('joining channel', channel?.display_name, channel.id); //eslint-disable-line
|
||||
const result = await dispatch(joinChannel(currentUserId, '', channel.id));
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch(handleSelectChannel(channel.id));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,11 +31,33 @@ jest.mock('@mm-redux/actions/channels', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('@mm-redux/actions/teams', () => {
|
||||
const teamActions = jest.requireActual('../../mm-redux/actions/teams');
|
||||
return {
|
||||
...teamActions,
|
||||
getTeamByName: jest.fn((teamName) => {
|
||||
if (teamName) {
|
||||
return {
|
||||
type: 'MOCK_RECEIVE_TEAM_TYPE',
|
||||
data: {
|
||||
id: 'current-team-id',
|
||||
name: 'received-team-id',
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'MOCK_ERROR',
|
||||
error: 'error',
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@mm-redux/selectors/entities/teams', () => {
|
||||
const teamSelectors = jest.requireActual('../../mm-redux/selectors/entities/teams');
|
||||
return {
|
||||
...teamSelectors,
|
||||
getTeamByName: jest.fn(() => ({name: 'current-team-name'})),
|
||||
selectTeamByName: jest.fn(() => ({name: 'current-team-name'})),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -51,8 +73,9 @@ describe('Actions.Views.Channel', () => {
|
|||
const MOCK_RECEIVED_POSTS_SINCE = 'MOCK_RECEIVED_POSTS_SINCE';
|
||||
|
||||
const actions = require('@mm-redux/actions/channels');
|
||||
actions.getChannelByNameAndTeamName = jest.fn((teamName) => {
|
||||
if (teamName) {
|
||||
|
||||
actions.getChannelByName = jest.fn((teamId, channelName) => {
|
||||
if (teamId && channelName) {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: 'received-channel-id',
|
||||
|
|
@ -140,6 +163,9 @@ describe('Actions.Views.Channel', () => {
|
|||
name: currentTeamName,
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
[currentTeamId]: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -183,7 +209,7 @@ describe('Actions.Views.Channel', () => {
|
|||
|
||||
test('handleSelectChannelByName failure from no permission to channel', async () => {
|
||||
store = mockStore({...storeObj});
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
actions.getChannelByName = jest.fn(() => {
|
||||
return {
|
||||
type: 'MOCK_ERROR',
|
||||
error: {
|
||||
|
|
@ -212,7 +238,7 @@ describe('Actions.Views.Channel', () => {
|
|||
});
|
||||
|
||||
test('handleSelectChannelByName select channel that user is not a member of', async () => {
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
actions.getChannelByName = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL},
|
||||
|
|
@ -239,7 +265,7 @@ describe('Actions.Views.Channel', () => {
|
|||
store = mockStore(archivedChannelStoreObj);
|
||||
|
||||
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
actions.getChannelByName = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
|
||||
|
|
@ -266,7 +292,7 @@ describe('Actions.Views.Channel', () => {
|
|||
store = mockStore(noArchivedChannelStoreObj);
|
||||
|
||||
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
actions.getChannelByName = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
|
|||
onClose: () => {
|
||||
dispatch(closePermalink());
|
||||
},
|
||||
teamName,
|
||||
};
|
||||
|
||||
const options = {
|
||||
|
|
|
|||
|
|
@ -255,7 +255,10 @@ class ChannelIntro extends PureComponent {
|
|||
|
||||
return (
|
||||
<View>
|
||||
<Text style={style.channelTitle}>
|
||||
<Text
|
||||
testID='channel_intro.beginning.text'
|
||||
style={style.channelTitle}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: 'intro_messages.beginning',
|
||||
defaultMessage: 'Beginning of {name}',
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export default class MarkdownLink extends PureComponent {
|
|||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
const {intl} = this.context;
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel.bind(null, intl));
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel.bind(null, intl), intl);
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
if (match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
onPermalinkPress(match.postId, this.props.currentTeamName);
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export default class PostList extends PureComponent {
|
|||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
const {intl} = this.context;
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel(intl));
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel(intl), intl);
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
if (match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
this.handlePermalinkPress(match.postId, currentTeamName);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {Client4} from '@mm-redux/client';
|
|||
import {General, Preferences} from '../constants';
|
||||
import {ChannelTypes, PreferenceTypes, TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {savePreferences, deletePreferences} from './preferences';
|
||||
import {compareNotifyProps, getChannelsIdForTeam, getChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
import {compareNotifyProps, getChannelsIdForTeam, getChannelByName as selectChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
import {
|
||||
getChannelsNameMapInTeam,
|
||||
getMyChannelMember as getMyChannelMemberSelector,
|
||||
|
|
@ -438,6 +438,29 @@ export function getChannelByNameAndTeamName(teamName: string, channelName: strin
|
|||
};
|
||||
}
|
||||
|
||||
export function getChannelByName(teamId: string, channelName: string, includeDeleted = false): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
let channel;
|
||||
try {
|
||||
channel = await Client4.getChannelByName(teamId, channelName, includeDeleted);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(batchActions([
|
||||
{type: ChannelTypes.CHANNELS_FAILURE, error},
|
||||
logError(error),
|
||||
]));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: channel,
|
||||
});
|
||||
|
||||
return {data: channel};
|
||||
};
|
||||
}
|
||||
|
||||
export function getChannel(channelId: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
let data;
|
||||
|
|
@ -739,7 +762,7 @@ export function deleteChannel(channelId: string): ActionFunc {
|
|||
if (channelId === currentChannelId && !viewArchivedChannels) {
|
||||
const teamId = getCurrentTeamId(state);
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
const channel = selectChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
if (channel && channel.id) {
|
||||
dispatch({type: ChannelTypes.SELECT_CHANNEL, data: channel.id});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,7 +424,7 @@ export function handleGotoLocation(href: string, intl: any): ActionFunc {
|
|||
if (match) {
|
||||
switch (match.type) {
|
||||
case DeepLinkTypes.CHANNEL:
|
||||
dispatch(handleSelectChannelByName(match.channelName, match.teamName, () => DraftUtils.errorBadChannel(intl)));
|
||||
dispatch(handleSelectChannelByName(match.channelName, match.teamName, () => DraftUtils.errorBadChannel(intl), intl));
|
||||
break;
|
||||
case DeepLinkTypes.PERMALINK: {
|
||||
const {error} = await dispatch(loadChannelsByTeamName(match.teamName, () => permalinkBadTeam(intl)));
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ import {getPostsAround, getPostThread} from '@actions/views/post';
|
|||
import {handleTeamChange} from '@actions/views/select_team';
|
||||
import {getChannel as getChannelAction, joinChannel} from '@mm-redux/actions/channels';
|
||||
import {selectPost} from '@mm-redux/actions/posts';
|
||||
import {addUserToTeam, getTeamByName, removeUserFromTeam} from '@mm-redux/actions/teams';
|
||||
import {makeGetChannel, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {makeGetPostIdsAroundPost, getPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentTeamId, getTeamByName as selectTeamByName, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import Permalink from './permalink';
|
||||
|
|
@ -21,7 +22,7 @@ function makeMapStateToProps() {
|
|||
const getPostIdsAroundPost = makeGetPostIdsAroundPost();
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
return function mapStateToProps(state) {
|
||||
return function mapStateToProps(state, props) {
|
||||
const {currentFocusedPostId} = state.entities.posts;
|
||||
const post = getPost(state, currentFocusedPostId);
|
||||
|
||||
|
|
@ -44,8 +45,10 @@ function makeMapStateToProps() {
|
|||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
focusedPostId: currentFocusedPostId,
|
||||
myMembers: getMyChannelMemberships(state),
|
||||
myChannelMemberships: getMyChannelMemberships(state),
|
||||
myTeamMemberships: getTeamMemberships(state),
|
||||
postIds,
|
||||
team: selectTeamByName(state, props.teamName),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
|
|
@ -54,12 +57,15 @@ function makeMapStateToProps() {
|
|||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addUserToTeam,
|
||||
getPostsAround,
|
||||
getPostThread,
|
||||
getChannel: getChannelAction,
|
||||
getTeamByName,
|
||||
handleSelectChannel,
|
||||
handleTeamChange,
|
||||
joinChannel,
|
||||
removeUserFromTeam,
|
||||
selectPost,
|
||||
}, dispatch),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import SafeAreaView from '@components/safe_area_view';
|
|||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {getLastPostIndex} from '@mm-redux/utils/post_list';
|
||||
import {privateChannelJoinPrompt} from '@utils/channels';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -52,12 +53,15 @@ Animatable.initializeRegistryWithDefinitions({
|
|||
export default class Permalink extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
addUserToTeam: PropTypes.func.isRequired,
|
||||
getPostsAround: PropTypes.func.isRequired,
|
||||
getPostThread: PropTypes.func.isRequired,
|
||||
getChannel: PropTypes.func.isRequired,
|
||||
getTeamByName: PropTypes.func.isRequired,
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
handleTeamChange: PropTypes.func.isRequired,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
removeUserFromTeam: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string,
|
||||
|
|
@ -68,9 +72,12 @@ export default class Permalink extends PureComponent {
|
|||
currentUserId: PropTypes.string.isRequired,
|
||||
focusedPostId: PropTypes.string.isRequired,
|
||||
isPermalink: PropTypes.bool,
|
||||
myMembers: PropTypes.object.isRequired,
|
||||
myChannelMemberships: PropTypes.object.isRequired,
|
||||
myTeamMemberships: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
postIds: PropTypes.array,
|
||||
team: PropTypes.object,
|
||||
teamName: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
error: PropTypes.string,
|
||||
};
|
||||
|
|
@ -96,6 +103,7 @@ export default class Permalink extends PureComponent {
|
|||
this.state = {
|
||||
title: '',
|
||||
loading,
|
||||
joinChannelPromptVisible: false,
|
||||
error: error || '',
|
||||
retry: false,
|
||||
};
|
||||
|
|
@ -240,13 +248,55 @@ export default class Permalink extends PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
if (!focusChannelId) {
|
||||
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
|
||||
focusChannelId = focusedPost ? focusedPost.channel_id : '';
|
||||
if (focusChannelId) {
|
||||
const {data: channel} = await actions.getChannel(focusChannelId);
|
||||
if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) {
|
||||
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
|
||||
}
|
||||
|
||||
if (focusChannelId) {
|
||||
const {teamName} = this.props;
|
||||
let {team} = this.props;
|
||||
if (!team) {
|
||||
const teamResponse = await actions.getTeamByName(teamName);
|
||||
if (teamResponse.error) {
|
||||
this.setState({error: teamResponse.error.message, loading: false});
|
||||
return;
|
||||
}
|
||||
team = teamResponse.data;
|
||||
}
|
||||
let joinedNewTeam = false;
|
||||
if (!this.props.myTeamMemberships[team.id]) {
|
||||
const teamJoinResponse = await actions.addUserToTeam(team.id, currentUserId);
|
||||
if (teamJoinResponse.error) {
|
||||
this.setState({error: teamJoinResponse.error.message, loading: false});
|
||||
return;
|
||||
}
|
||||
joinedNewTeam = true;
|
||||
}
|
||||
if (!this.props.myChannelMemberships[focusChannelId]) {
|
||||
const {error: channelError, data: channel} = await actions.getChannel(focusChannelId);
|
||||
if (channelError) {
|
||||
this.setState({error: channelError.message, loading: false});
|
||||
} else {
|
||||
if (channel.type === General.PRIVATE_CHANNEL) {
|
||||
this.setState({joinChannelPromptVisible: true});
|
||||
const {join} = await privateChannelJoinPrompt(channel, this.context.intl);
|
||||
if (!join) {
|
||||
if (joinedNewTeam) {
|
||||
await actions.removeUserFromTeam(team.id, currentUserId);
|
||||
}
|
||||
this.handleClose();
|
||||
return;
|
||||
}
|
||||
this.setState({joinChannelPromptVisible: false});
|
||||
}
|
||||
|
||||
// Join Open/Private channel
|
||||
const channelJoinResponse = await actions.joinChannel(currentUserId, channel.team_id, channel.id);
|
||||
if (channelJoinResponse.error) {
|
||||
this.setState({error: channelJoinResponse.error.message, loading: false});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -286,7 +336,7 @@ export default class Permalink extends PureComponent {
|
|||
|
||||
render() {
|
||||
const {channelName, currentUserId, focusedPostId, postIds, theme} = this.props;
|
||||
const {error, loading, retry, title} = this.state;
|
||||
const {error, joinChannelPromptVisible, loading, retry, title} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let postList;
|
||||
|
|
@ -306,7 +356,7 @@ export default class Permalink extends PureComponent {
|
|||
</View>
|
||||
);
|
||||
} else if (loading) {
|
||||
postList = <Loading color={theme.centerChannelColor}/>;
|
||||
postList = joinChannelPromptVisible ? null : <Loading color={theme.centerChannelColor}/>;
|
||||
} else {
|
||||
postList = (
|
||||
<PostList
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import {getLastCreateAt} from '@mm-redux/utils/post_utils';
|
||||
|
|
@ -76,3 +76,44 @@ export function getChannelSinceValue(state, channelId, postIds) {
|
|||
|
||||
return since;
|
||||
}
|
||||
|
||||
export function privateChannelJoinPrompt(channel, intl) {
|
||||
return new Promise((resolve) => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'permalink.show_dialog_warn.title',
|
||||
defaultMessage: 'Join private channel',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'permalink.show_dialog_warn.description',
|
||||
defaultMessage: 'You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?',
|
||||
}, {
|
||||
channel: channel.display_name,
|
||||
}),
|
||||
[
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'permalink.show_dialog_warn.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
onPress: async () => {
|
||||
resolve({
|
||||
join: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'permalink.show_dialog_warn.join',
|
||||
defaultMessage: 'Join',
|
||||
}),
|
||||
onPress: async () => {
|
||||
resolve({
|
||||
join: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -564,6 +564,10 @@
|
|||
"password_send.reset": "Reset my password",
|
||||
"permalink.error.access": "Permalink belongs to a deleted message or to a channel to which you do not have access.",
|
||||
"permalink.error.link_not_found": "Link Not Found",
|
||||
"permalink.show_dialog_warn.cancel": "Cancel",
|
||||
"permalink.show_dialog_warn.description": "You are about to join \"{channel}\" without explicitly being added by the channel admin. Are you sure you wish to join this private channel?",
|
||||
"permalink.show_dialog_warn.join": "Join",
|
||||
"permalink.show_dialog_warn.title": "Join private channel",
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel.",
|
||||
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
||||
"post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ class Alert {
|
|||
// alert titles
|
||||
archivePublicChannelTitle = isAndroid() ? element(by.text('Archive Public Channel')) : element(by.label('Archive Public Channel')).atIndex(0);
|
||||
deletePostTitle = isAndroid() ? element(by.text('Delete Post')) : element(by.label('Delete Post')).atIndex(0);
|
||||
joinPrivateChannelTitle = isAndroid() ? element(by.text('Join private channel')) : element(by.label('Join private channel')).atIndex(0);
|
||||
|
||||
// alert buttons
|
||||
cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(0);
|
||||
deleteButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(0);
|
||||
joinButton = isAndroid() ? element(by.text('JOIN')) : element(by.label('Join')).atIndex(0);
|
||||
noButton = isAndroid() ? element(by.text('NO')) : element(by.label('No')).atIndex(0);
|
||||
yesButton = isAndroid() ? element(by.text('YES')) : element(by.label('Yes')).atIndex(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,18 @@
|
|||
// - Use element testID when selecting an element. Create one if none.
|
||||
// *******************************************************************
|
||||
|
||||
import {MainSidebar} from '@support/ui/component';
|
||||
import {ChannelScreen} from '@support/ui/screen';
|
||||
import {Alert, MainSidebar} from '@support/ui/component';
|
||||
import {ChannelScreen, CreateChannelScreen, PermalinkScreen} from '@support/ui/screen';
|
||||
|
||||
import {Setup, Team, User} from '@support/server_api';
|
||||
import {serverUrl} from '@support/test_config';
|
||||
import {Channel, Post, Setup, Team, User} from '@support/server_api';
|
||||
import {adminUsername, adminPassword, serverUrl} from '@support/test_config';
|
||||
import {getRandomId} from '@support/utils';
|
||||
|
||||
describe('Messaging', () => {
|
||||
let testChannel;
|
||||
let testTeam;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
const {channel, team, user} = await Setup.apiInit();
|
||||
testChannel = channel;
|
||||
testTeam = team;
|
||||
|
|
@ -25,20 +26,19 @@ describe('Messaging', () => {
|
|||
await ChannelScreen.open(user);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ChannelScreen.logout();
|
||||
});
|
||||
|
||||
it('MM-T3471 Tapping channel URL link joins public channel', async () => {
|
||||
const {
|
||||
channelNavBarTitle,
|
||||
logout,
|
||||
openMainSidebar,
|
||||
postMessage,
|
||||
} = ChannelScreen;
|
||||
const {getChannelByDisplayName} = MainSidebar;
|
||||
|
||||
// # Go to the Town Square channel
|
||||
await openMainSidebar();
|
||||
let channelItem = getChannelByDisplayName('Town Square');
|
||||
await channelItem.tap();
|
||||
await expect(channelNavBarTitle).toHaveText('Town Square');
|
||||
await gotoChannel('Town Square');
|
||||
|
||||
// # There's no way to get a channel permalink on mobile so we make one manually
|
||||
const channelPermalink = `${serverUrl}/${testTeam.name}/channels/${testChannel.name}`;
|
||||
|
|
@ -52,16 +52,133 @@ describe('Messaging', () => {
|
|||
await Team.apiAddUserToTeam(otherUser.id, testTeam.id);
|
||||
await logout();
|
||||
await ChannelScreen.open(otherUser);
|
||||
await openMainSidebar();
|
||||
channelItem = getChannelByDisplayName('Town Square');
|
||||
await channelItem.tap();
|
||||
await expect(channelNavBarTitle).toHaveText('Town Square');
|
||||
await gotoChannel('Town Square');
|
||||
|
||||
// # As this new user, tap the channel permalink we posted earlier
|
||||
const permalinkPost = element(by.text(channelPermalink));
|
||||
await permalinkPost.tap({x: 5, y: 10});
|
||||
await tapLink(channelPermalink);
|
||||
|
||||
// * Confirm that we have joined the correct channel from the channel permalink
|
||||
await expect(channelNavBarTitle).toHaveText(testChannel.display_name);
|
||||
});
|
||||
|
||||
// - Create two private channels
|
||||
// - Post first channel url link in the public channel
|
||||
// - Post a message in second channel and post the permalink of it in the public channel
|
||||
// - Confirm the prompt and join the channel
|
||||
it('MM-30237 System admins prompted before joining private channel via permalink', async () => {
|
||||
const {
|
||||
logout,
|
||||
openTeamSidebar,
|
||||
postMessage,
|
||||
} = ChannelScreen;
|
||||
const {getTeamByDisplayName} = MainSidebar;
|
||||
|
||||
// # Create Private Channel 1
|
||||
const privateChannel1Name = 'pc' + getRandomId();
|
||||
await createPrivateChannel(privateChannel1Name);
|
||||
|
||||
// # Create Private Channel 2
|
||||
const privateChannel2Name = 'pc' + getRandomId();
|
||||
await createPrivateChannel(privateChannel2Name);
|
||||
|
||||
// # Post a message in private channel 2
|
||||
await postMessage(Date.now().toString());
|
||||
|
||||
// # Get the last post data
|
||||
const {channel: privateChannel2} = await Channel.apiGetChannelByName(testTeam.name, privateChannel2Name);
|
||||
const {post} = await Post.apiGetLastPostInChannel(privateChannel2.id);
|
||||
|
||||
// # Go to the Town Square channel
|
||||
await gotoChannel('Town Square');
|
||||
|
||||
// # Post Private Channel 1 Permalink
|
||||
const message1 = `${serverUrl}/${testTeam.name}/channels/${privateChannel1Name}`;
|
||||
await postMessage(message1);
|
||||
|
||||
// * Check that message is successfully posted
|
||||
await expect(element(by.text(message1))).toExist();
|
||||
|
||||
// # Post Private Channel 2's POST Permalink
|
||||
const message2 = `${serverUrl}/${testTeam.name}/pl/${post.id}`;
|
||||
await postMessage(message2);
|
||||
|
||||
// * Check that message is successfully posted
|
||||
await expect(element(by.text(message2))).toExist();
|
||||
|
||||
// # Logout and login as sysadmin
|
||||
await logout();
|
||||
await ChannelScreen.open({
|
||||
username: adminUsername,
|
||||
password: adminPassword,
|
||||
});
|
||||
|
||||
// * Verify channel screen is visible
|
||||
await ChannelScreen.toBeVisible();
|
||||
|
||||
// # Go to the team
|
||||
await openTeamSidebar();
|
||||
await getTeamByDisplayName(testTeam.display_name).tap();
|
||||
|
||||
// # Press on message 1
|
||||
await tapLink(message1);
|
||||
|
||||
// # Press on Join button
|
||||
await joinPrivateChannel();
|
||||
|
||||
// * Confirm joining the "private channel 1"
|
||||
await expect(ChannelScreen.channelIntro).toHaveText('Beginning of ' + privateChannel1Name);
|
||||
|
||||
// # Go to Townsquare
|
||||
await gotoChannel('Town Square');
|
||||
|
||||
// # Press on message 2
|
||||
await tapLink(message2);
|
||||
|
||||
// # Press on Join button
|
||||
await joinPrivateChannel();
|
||||
|
||||
// * Verify permalink post list has the message
|
||||
await PermalinkScreen.toBeVisible();
|
||||
await expect(element(by.text(post.message))).toBeVisible();
|
||||
|
||||
// # Jump to recent messages
|
||||
await PermalinkScreen.jumpToRecentMessages();
|
||||
|
||||
// * Verify user is on channel where message is posted
|
||||
await expect(ChannelScreen.channelIntro).toHaveText('Beginning of ' + privateChannel2Name);
|
||||
});
|
||||
});
|
||||
|
||||
async function createPrivateChannel(channelName) {
|
||||
// # Open Mainside bar and press on private channels more button
|
||||
await ChannelScreen.openMainSidebar();
|
||||
await MainSidebar.openCreatePrivateChannelButton.tap();
|
||||
|
||||
// * Verify create channel screen is visible
|
||||
await CreateChannelScreen.toBeVisible();
|
||||
await expect(element(by.text('New Private Channel'))).toBeVisible();
|
||||
|
||||
// # Fill the data and create a private channel
|
||||
await CreateChannelScreen.nameInput.typeText(channelName);
|
||||
await CreateChannelScreen.createButton.tap();
|
||||
|
||||
// * Expect a redirection to the created channel
|
||||
await expect(ChannelScreen.channelIntro).toHaveText('Beginning of ' + channelName);
|
||||
}
|
||||
|
||||
async function gotoChannel(name) {
|
||||
await ChannelScreen.openMainSidebar();
|
||||
const channelItem = MainSidebar.getChannelByDisplayName(name);
|
||||
await channelItem.tap();
|
||||
await expect(ChannelScreen.channelNavBarTitle).toHaveText(name);
|
||||
}
|
||||
|
||||
async function joinPrivateChannel() {
|
||||
await expect(Alert.joinPrivateChannelTitle).toBeVisible();
|
||||
await Alert.joinButton.tap();
|
||||
}
|
||||
|
||||
async function tapLink(message) {
|
||||
const permalinkPost = element(by.text(message));
|
||||
await permalinkPost.tap({x: 5, y: 10});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue