diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index 8f59e6587..02a4f2133 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -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));
}
diff --git a/app/actions/views/channel.test.js b/app/actions/views/channel.test.js
index 199141dce..ab09a7327 100644
--- a/app/actions/views/channel.test.js
+++ b/app/actions/views/channel.test.js
@@ -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},
diff --git a/app/actions/views/permalink.ts b/app/actions/views/permalink.ts
index e169cb7c9..d0e502a7d 100644
--- a/app/actions/views/permalink.ts
+++ b/app/actions/views/permalink.ts
@@ -28,6 +28,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
onClose: () => {
dispatch(closePermalink());
},
+ teamName,
};
const options = {
diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js
index 62e23333d..046e9689a 100644
--- a/app/components/channel_intro/channel_intro.js
+++ b/app/components/channel_intro/channel_intro.js
@@ -255,7 +255,10 @@ class ChannelIntro extends PureComponent {
return (
-
+
{intl.formatMessage({
id: 'intro_messages.beginning',
defaultMessage: 'Beginning of {name}',
diff --git a/app/components/markdown/markdown_link/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js
index be3ea8070..042dfa9c4 100644
--- a/app/components/markdown/markdown_link/markdown_link.js
+++ b/app/components/markdown/markdown_link/markdown_link.js
@@ -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);
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index ee1960682..52a75e30f 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -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);
diff --git a/app/mm-redux/actions/channels.ts b/app/mm-redux/actions/channels.ts
index c244b875a..986bcb52a 100644
--- a/app/mm-redux/actions/channels.ts
+++ b/app/mm-redux/actions/channels.ts
@@ -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});
}
diff --git a/app/mm-redux/actions/integrations.ts b/app/mm-redux/actions/integrations.ts
index f08ff92ff..4c6a3c3ed 100644
--- a/app/mm-redux/actions/integrations.ts
+++ b/app/mm-redux/actions/integrations.ts
@@ -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)));
diff --git a/app/screens/permalink/index.js b/app/screens/permalink/index.js
index fe2f0f156..46fb5fdbe 100644
--- a/app/screens/permalink/index.js
+++ b/app/screens/permalink/index.js
@@ -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),
};
diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js
index 52075c669..51120599f 100644
--- a/app/screens/permalink/permalink.js
+++ b/app/screens/permalink/permalink.js
@@ -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 {
);
} else if (loading) {
- postList = ;
+ postList = joinChannelPromptVisible ? null : ;
} else {
postList = (
{
+ 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,
+ });
+ },
+ },
+ ],
+ );
+ });
+}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 987677739..7d7b873ae 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/detox/e2e/support/ui/component/alert.js b/detox/e2e/support/ui/component/alert.js
index ab007dd8d..5e0268c87 100644
--- a/detox/e2e/support/ui/component/alert.js
+++ b/detox/e2e/support/ui/component/alert.js
@@ -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);
}
diff --git a/detox/e2e/test/messaging/tapping_channel_url_link_joins_channel.e2e.js b/detox/e2e/test/messaging/tapping_channel_url_link_joins_channel.e2e.js
index e1b36c2a3..48f8602c8 100644
--- a/detox/e2e/test/messaging/tapping_channel_url_link_joins_channel.e2e.js
+++ b/detox/e2e/test/messaging/tapping_channel_url_link_joins_channel.e2e.js
@@ -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});
+}