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