diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js
index 5013971bd..c2ac366d8 100644
--- a/app/components/client_upgrade_listener/client_upgrade_listener.js
+++ b/app/components/client_upgrade_listener/client_upgrade_listener.js
@@ -62,17 +62,21 @@ export default class ClientUpgradeListener extends PureComponent {
}
}
- componentWillReceiveProps(nextProps) {
- const {forceUpgrade, latestVersion, minVersion} = this.props;
- const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = nextProps;
+ setTop(top) {
+ this.setState({top});
+ }
+
+ componentDidUpdate(prevProps) {
+ const {forceUpgrade, latestVersion, minVersion} = prevProps;
+ const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = this.props;
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
- this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
- } else if (this.props.isLandscape !== nextProps.isLandscape &&
+ this.checkUpgrade(minVersion, latestVersion, this.props.isLandscape);
+ } else if (prevProps.isLandscape !== this.props.isLandscape &&
isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_WITH_INSETS) {
- const newTop = nextProps.isLandscape ? 45 : 100;
- this.setState({top: new Animated.Value(newTop)});
+ const newTop = this.props.isLandscape ? 45 : 100;
+ this.setTop(new Animated.Value(newTop));
}
}
diff --git a/app/screens/select_team/__snapshots__/select_team.test.js.snap b/app/screens/select_team/__snapshots__/select_team.test.js.snap
index 6f792a6b7..883147cca 100644
--- a/app/screens/select_team/__snapshots__/select_team.test.js.snap
+++ b/app/screens/select_team/__snapshots__/select_team.test.js.snap
@@ -42,6 +42,7 @@ exports[`SelectTeam should match snapshot for teams 1`] = `
"flex": 1,
}
}
+ testID="select_team.screen"
>
diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js
index 305e946ca..a26c22d0e 100644
--- a/app/screens/select_team/select_team.js
+++ b/app/screens/select_team/select_team.js
@@ -26,6 +26,7 @@ import {resetToChannel, dismissModal} from 'app/actions/navigation';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
+import memoize from 'memoize-one';
const TEAMS_PER_PAGE = 50;
@@ -68,12 +69,6 @@ export default class SelectTeam extends PureComponent {
this.getTeams();
}
- componentWillReceiveProps(nextProps) {
- if (this.props.teams !== nextProps.teams) {
- this.buildData(nextProps);
- }
- }
-
navigationButtonPressed({buttonId}) {
const {logout} = this.props.actions;
@@ -98,17 +93,15 @@ export default class SelectTeam extends PureComponent {
});
}
- buildData = (props) => {
- if (props.teams.length) {
- this.setState({teams: props.teams});
- } else {
- const teams = [{
- id: t('mobile.select_team.no_teams'),
- defaultMessage: 'There are no available teams for you to join.',
- }];
- this.setState({teams});
+ memoizedTeams = memoize((teams) => {
+ if (teams.length) {
+ return teams;
}
- };
+ return [{
+ id: t('mobile.select_team.no_teams'),
+ defaultMessage: 'There are no available teams for you to join.',
+ }];
+ })
close = () => {
dismissModal();
@@ -164,6 +157,7 @@ export default class SelectTeam extends PureComponent {
+
this.onSelectTeam(item))}
>
-
+
+
+
@@ -256,6 +270,7 @@ export default class SelectTeam extends PureComponent {
{
}
};
+/**
+ * Remove user from channel.
+ * See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}~1members~1{user_id}/delete
+ * @param {string} channelId - The channel ID
+ * @param {string} userId - The user ID to be removed from channel
+ * @return {Object} returns status on success or {error, status} on error
+ */
+export const apiDeleteUserFromChannel = async (channelId, userId) => {
+ try {
+ const response = await client.delete(
+ `/api/v4/channels/${channelId}/members/${userId}`,
+ );
+
+ return response;
+ } catch (err) {
+ return getResponseFromError(err);
+ }
+};
+
+/**
+ * Get channels for user.
+ * See https://api.mattermost.com/#tag/channels/paths/~1users~1{user_id}~1teams~1{team_id}~1channels/get
+ * @param {string} userId - The user ID
+ * @param {string} teamId - The team ID the user belongs to
+ * @return {Object} returns {channels} on success or {error, status} on error
+ */
+export const apiGetChannelsForUser = async (userId, teamId) => {
+ try {
+ const response = await client.get(`/api/v4/users/${userId}/teams/${teamId}/channels`);
+
+ return {channels: response.data};
+ } catch (err) {
+ return getResponseFromError(err);
+ }
+};
+
function generateRandomChannel(teamId, type, prefix) {
const randomId = getRandomId();
@@ -93,7 +129,9 @@ function generateRandomChannel(teamId, type, prefix) {
export const Channel = {
apiAddUserToChannel,
apiCreateChannel,
+ apiDeleteUserFromChannel,
apiGetChannelByName,
+ apiGetChannelsForUser,
};
export default Channel;
diff --git a/detox/e2e/support/server_api/team.js b/detox/e2e/support/server_api/team.js
index ad34124fe..7a52c9b02 100644
--- a/detox/e2e/support/server_api/team.js
+++ b/detox/e2e/support/server_api/team.js
@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import jestExpect from 'expect';
import {capitalize, getRandomId} from '@support/utils';
import client from './client';
@@ -60,12 +61,80 @@ export const apiAddUserToTeam = async (userId, teamId) => {
};
/**
- * Get team members for user.
- * See https://api.mattermost.com/#tag/teams/paths/~1users~1{user_id}~1teams~1members/get
- * @param {string} userId
+ * Delete a team.
+ * See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}/delete
+ * @param {string} teamId - The team ID
+ * @return {Object} returns {status} on success or {error, status} on error
+ */
+export const apiDeleteTeam = async (teamId) => {
+ try {
+ const response = await client.delete(
+ `/api/v4/teams/${teamId}`,
+ );
+
+ return {status: response.status};
+ } catch (err) {
+ return getResponseFromError(err);
+ }
+};
+
+/**
+ * Delete teams.
+ * @param {Array} teams - array of teams
+ */
+export const apiDeleteTeams = async (teams = []) => {
+ let teamArray = teams;
+ if (!teamArray.length > 0) {
+ ({teams: teamArray} = await Team.apiGetTeams());
+ }
+
+ teamArray.forEach(async (team) => {
+ const {status} = await Team.apiDeleteTeam(team.id);
+ jestExpect(status).toEqual(200);
+ });
+};
+
+/**
+ * Remove user from team.
+ * See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members~1{user_id}/delete
+ * @param {string} teamId - The team ID
+ * @param {string} userId - The user ID to be removed from team
+ * @return {Object} returns {status} on success or {error, status} on error
+ */
+export const apiDeleteUserFromTeam = async (teamId, userId) => {
+ try {
+ const response = await client.delete(
+ `/api/v4/teams/${teamId}/members/${userId}`,
+ );
+
+ return {status: response.status};
+ } catch (err) {
+ return getResponseFromError(err);
+ }
+};
+
+/**
+ * Get teams.
+ * See https://api.mattermost.com/#tag/teams/paths/~1teams/get
* @return {Object} returns {teams} on success or {error, status} on error
*/
-export const apiGetTeamMembersForUser = async (userId = 'me') => {
+export const apiGetTeams = async () => {
+ try {
+ const response = await client.get('/api/v4/teams');
+
+ return {teams: response.data};
+ } catch (err) {
+ return getResponseFromError(err);
+ }
+};
+
+/**
+ * Get teams for user.
+ * See https://api.mattermost.com/#tag/teams/paths/~1users~1{user_id}~1teams/get
+ * @param {string} userId - The user ID
+ * @return {Object} returns {teams} on success or {error, status} on error
+ */
+export const apiGetTeamsForUser = async (userId = 'me') => {
try {
const response = await client.get(`/api/v4/users/${userId}/teams`);
@@ -75,6 +144,52 @@ export const apiGetTeamMembersForUser = async (userId = 'me') => {
}
};
+/**
+ * Patch a team.
+ * See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1patch/put
+ * @param {string} teamId - The team ID
+ * @param {string} patch.display_name - Display name
+ * @param {string} patch.description - Description
+ * @param {string} patch.company_name - Company name
+ * @param {string} patch.allowed_domains - Allowed domains
+ * @param {boolean} patch.allow_open_invite - Allow open invite
+ * @param {boolean} patch.group_constrained - Group constrained
+ * @return {Object} returns {team} on success or {error, status} on error
+ */
+export const apiPatchTeam = async (teamId, teamData) => {
+ try {
+ const response = await client.put(
+ `/api/v4/teams/${teamId}/patch`,
+ teamData,
+ );
+
+ return {team: response.data};
+ } catch (err) {
+ return getResponseFromError(err);
+ }
+};
+
+/**
+ * Patch teams.
+ * @param {string} patch.display_name - Display name
+ * @param {string} patch.description - Description
+ * @param {string} patch.company_name - Company name
+ * @param {string} patch.allowed_domains - Allowed domains
+ * @param {boolean} patch.allow_open_invite - Allow open invite
+ * @param {boolean} patch.group_constrained - Group constrained
+ * @param {Array} teams - array of teams
+ */
+export const apiPatchTeams = async (teamData, teams = []) => {
+ let teamArray = teams;
+ if (!teamArray.length > 0) {
+ ({teams: teamArray} = await Team.apiGetTeams());
+ }
+
+ teamArray.forEach(async (team) => {
+ await Team.apiPatchTeam(team.id, teamData);
+ });
+};
+
function generateRandomTeam(type, prefix) {
const randomId = getRandomId();
@@ -88,7 +203,13 @@ function generateRandomTeam(type, prefix) {
export const Team = {
apiAddUserToTeam,
apiCreateTeam,
- apiGetTeamMembersForUser,
+ apiDeleteTeam,
+ apiDeleteTeams,
+ apiDeleteUserFromTeam,
+ apiGetTeams,
+ apiGetTeamsForUser,
+ apiPatchTeam,
+ apiPatchTeams,
};
export default Team;
diff --git a/detox/e2e/support/ui/screen/index.js b/detox/e2e/support/ui/screen/index.js
index ba15d56f3..9a87f0fd4 100644
--- a/detox/e2e/support/ui/screen/index.js
+++ b/detox/e2e/support/ui/screen/index.js
@@ -21,6 +21,7 @@ import RecentMentionsScreen from './recent_mentions';
import SearchResultPostScreen from './search_result_post';
import SearchScreen from './search';
import SelectServerScreen from './select_server';
+import SelectTeamScreen from './select_team';
import ThreadScreen from './thread';
export {
@@ -44,5 +45,6 @@ export {
SearchResultPostScreen,
SearchScreen,
SelectServerScreen,
+ SelectTeamScreen,
ThreadScreen,
};
diff --git a/detox/e2e/support/ui/screen/select_team.js b/detox/e2e/support/ui/screen/select_team.js
new file mode 100644
index 000000000..4ecf32f69
--- /dev/null
+++ b/detox/e2e/support/ui/screen/select_team.js
@@ -0,0 +1,55 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+class SelectTeamScreen {
+ testID = {
+ selectTeamScreen: 'select_team.screen',
+ noTeams: 'select_team.no_teams',
+ teamsList: 'select_team.custom_list',
+ teamItem: 'select_team.custom_list.team_item',
+ teamItemDisplayName: 'select_team.custom_list.team_item.display_name',
+ teamItemIcon: 'select_team.custom_list.team_item.team_icon',
+ teamItemIconContent: 'select_team.custom_list.team_item.team_icon.content',
+ }
+
+ selectTeamScreen = element(by.id(this.testID.selectTeamScreen));
+ noTeams = element(by.id(this.testID.noTeams));
+ teamsList = element(by.id(this.testID.teamsList));
+
+ toBeVisible = async () => {
+ await expect(this.selectTeamScreen).toBeVisible();
+
+ return this.selectTeamScreen;
+ }
+
+ getTeamItem = (teamId, displayName) => {
+ const teamItemTestID = `${this.testID.teamItem}.${teamId}`;
+ const baseMatcher = by.id(teamItemTestID);
+ const teamItemMatcher = displayName ? baseMatcher.withDescendant(by.text(displayName)) : baseMatcher;
+ const teamItemDisplayNameMatcher = by.id(this.testID.teamItemDisplayName).withAncestor(teamItemMatcher);
+ const teamItemIconMatcher = by.id(this.testID.teamItemIcon).withAncestor(teamItemMatcher);
+ const teamItemIconContentMatcher = by.id(this.testID.teamItemIconContent).withAncestor(teamItemMatcher);
+
+ return {
+ teamItem: element(teamItemMatcher),
+ teamItemDisplayName: element(teamItemDisplayNameMatcher),
+ teamItemIcon: element(teamItemIconMatcher),
+ teamItemIconContent: element(teamItemIconContentMatcher),
+ };
+ }
+
+ getTeamByDisplayName = (displayName) => {
+ return element(by.text(displayName).withAncestor(by.id(this.testID.teamsList)));
+ }
+
+ getTeamDisplayNameAtIndex = (index) => {
+ return element(by.id(this.testID.teamItemDisplayName)).atIndex(index);
+ }
+
+ getTeamIconContentAtIndex = (index) => {
+ return element(by.id(this.testID.teamItemIconContent)).atIndex(index);
+ }
+}
+
+const selectTeamScreen = new SelectTeamScreen();
+export default selectTeamScreen;
diff --git a/detox/e2e/test/smoke_test/login_ldap.e2e.js b/detox/e2e/test/smoke_test/login_ldap.e2e.js
index 52144f99d..fe0f2df16 100644
--- a/detox/e2e/test/smoke_test/login_ldap.e2e.js
+++ b/detox/e2e/test/smoke_test/login_ldap.e2e.js
@@ -65,7 +65,7 @@ async function ensureUserHasTeam(ldapUser) {
// # Login as sysadmin and ensure LDAP user is member of at least one team
await User.apiAdminLogin();
const {user} = await User.apiGetUserByUsername(ldapUser.username);
- const {teams} = await Team.apiGetTeamMembersForUser(user.id);
+ const {teams} = await Team.apiGetTeamsForUser(user.id);
if (!teams?.length) {
const {team} = await Setup.apiInit();
diff --git a/detox/e2e/test/teams/select_team.e2e.js b/detox/e2e/test/teams/select_team.e2e.js
new file mode 100644
index 000000000..8747dfade
--- /dev/null
+++ b/detox/e2e/test/teams/select_team.e2e.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// *******************************************************************
+// - [#] indicates a test step (e.g. # Go to a screen)
+// - [*] indicates an assertion (e.g. * Check the title)
+// - Use element testID when selecting an element. Create one if none.
+// *******************************************************************
+
+import {
+ ChannelScreen,
+ LoginScreen,
+ SelectTeamScreen,
+} from '@support/ui/screen';
+import {
+ Channel,
+ Team,
+ User,
+} from '@support/server_api';
+
+describe('Select Team', () => {
+ let testTeam;
+
+ beforeAll(async () => {
+ await Team.apiPatchTeams({allow_open_invite: false});
+ const {user} = await User.apiCreateUser();
+ const {team} = await Team.apiCreateTeam();
+ testTeam = team;
+ await Team.apiPatchTeam(testTeam.id, {allow_open_invite: true});
+
+ // # Open select team screen
+ await LoginScreen.open();
+ await LoginScreen.login(user);
+ await SelectTeamScreen.toBeVisible();
+ });
+
+ afterAll(async () => {
+ await ChannelScreen.logout();
+ });
+
+ it('MM-T3619 should be able to select a team', async () => {
+ // # Tap on team to join
+ const team = await SelectTeamScreen.getTeamByDisplayName(testTeam.display_name);
+ await team.tap();
+
+ // * Verify redirect to default channel of joined team
+ const {channel} = await Channel.apiGetChannelByName(testTeam.name, 'town-square');
+ const {channelNavBarTitle} = ChannelScreen;
+ await ChannelScreen.toBeVisible();
+ await expect(channelNavBarTitle).toHaveText(channel.display_name);
+ });
+});