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:
Anurag Shivarathri 2021-02-12 09:09:34 +05:30 committed by GitHub
parent 43645d781e
commit 3c51b39bf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 368 additions and 64 deletions

View file

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

View file

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

View file

@ -28,6 +28,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
onClose: () => {
dispatch(closePermalink());
},
teamName,
};
const options = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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