diff --git a/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap b/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap
index e864ae21a..6329d2e31 100644
--- a/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap
+++ b/app/components/post_body/__snapshots__/system_message_helpers.test.js.snap
@@ -35,3 +35,12 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
value="{username} archived the channel"
/>
`;
+
+exports[`renderSystemMessage uses renderer for unarchived channel 1`] = `
+
+`;
diff --git a/app/components/post_body/system_message_helpers.js b/app/components/post_body/system_message_helpers.js
index 2a9c2f569..1a3c537a3 100644
--- a/app/components/post_body/system_message_helpers.js
+++ b/app/components/post_body/system_message_helpers.js
@@ -144,11 +144,25 @@ const renderArchivedMessage = (postBodyProps, styles, intl) => {
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
};
+const renderUnarchivedMessage = (postBodyProps, styles, intl) => {
+ const {postProps} = postBodyProps;
+
+ const username = renderUsername(postProps.username);
+ const localeHolder = {
+ id: t('mobile.system_message.channel_unarchived_message'),
+ defaultMessage: '{username} unarchived the channel',
+ };
+
+ const values = {username};
+ return renderMessage(postBodyProps, styles, intl, localeHolder, values);
+};
+
const systemMessageRenderers = {
[Posts.POST_TYPES.HEADER_CHANGE]: renderHeaderChangeMessage,
[Posts.POST_TYPES.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage,
[Posts.POST_TYPES.PURPOSE_CHANGE]: renderPurposeChangeMessage,
[Posts.POST_TYPES.CHANNEL_DELETED]: renderArchivedMessage,
+ [Posts.POST_TYPES.CHANNEL_UNARCHIVED]: renderUnarchivedMessage,
};
export const renderSystemMessage = (postBodyProps, styles, intl) => {
diff --git a/app/components/post_body/system_message_helpers.test.js b/app/components/post_body/system_message_helpers.test.js
index e533edda0..d4f11734c 100644
--- a/app/components/post_body/system_message_helpers.test.js
+++ b/app/components/post_body/system_message_helpers.test.js
@@ -78,6 +78,19 @@ describe('renderSystemMessage', () => {
expect(renderedMessage).toMatchSnapshot();
});
+ test('uses renderer for unarchived channel', () => {
+ const postBodyProps = {
+ ...basePostBodyProps,
+ postProps: {
+ ...basePostBodyProps.postProps,
+ },
+ postType: Posts.POST_TYPES.CHANNEL_UNARCHIVED,
+ };
+
+ const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
+ expect(renderedMessage).toMatchSnapshot();
+ });
+
test('is null for non-qualifying system messages', () => {
const postBodyProps = {
...basePostBodyProps,
diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js
index 47b081137..a7da28140 100644
--- a/app/screens/channel_info/channel_info.js
+++ b/app/screens/channel_info/channel_info.js
@@ -33,6 +33,7 @@ export default class ChannelInfo extends PureComponent {
closeGMChannel: PropTypes.func.isRequired,
convertChannelToPrivate: PropTypes.func.isRequired,
deleteChannel: PropTypes.func.isRequired,
+ unarchiveChannel: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
getChannel: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
@@ -49,6 +50,7 @@ export default class ChannelInfo extends PureComponent {
componentId: PropTypes.string,
viewArchivedChannels: PropTypes.bool.isRequired,
canDeleteChannel: PropTypes.bool.isRequired,
+ canUnarchiveChannel: PropTypes.bool.isRequired,
currentChannel: PropTypes.object.isRequired,
currentChannelCreatorName: PropTypes.string,
currentChannelMemberCount: PropTypes.number,
@@ -173,14 +175,6 @@ export default class ChannelInfo extends PureComponent {
goToScreen(screen, title);
});
- handleLeave = () => {
- this.handleDeleteOrLeave('leave');
- };
-
- handleDelete = () => {
- this.handleDeleteOrLeave('delete');
- };
-
handleConfirmConvertToPrivate = preventDoubleTap(async () => {
const {actions, currentChannel} = this.props;
const result = await actions.convertChannelToPrivate(currentChannel.id);
@@ -234,58 +228,92 @@ export default class ChannelInfo extends PureComponent {
);
});
- handleDeleteOrLeave = preventDoubleTap((eventType) => {
+ handleLeave = preventDoubleTap(() => {
+ const title = {id: t('mobile.channel_info.alertTitleLeaveChannel'), defaultMessage: 'Leave {term}'};
+ const message = {
+ id: t('mobile.channel_info.alertMessageLeaveChannel'),
+ defaultMessage: 'Are you sure you want to leave the {term} {name}?',
+ };
+ const onPressAction = () => {
+ this.props.actions.leaveChannel(this.props.currentChannel, true).then(() => {
+ this.close();
+ });
+ };
+ this.alertAndHandleYesAction(title, message, onPressAction);
+ });
+
+ handleDelete = preventDoubleTap(() => {
+ const channel = this.props.currentChannel;
+ const title = {id: t('mobile.channel_info.alertTitleDeleteChannel'), defaultMessage: 'Archive {term}'};
+ const message = {
+ id: t('mobile.channel_info.alertMessageDeleteChannel'),
+ defaultMessage: 'Are you sure you want to archive the {term} {name}?',
+ };
+ const onPressAction = async () => {
+ const result = await this.props.actions.deleteChannel(channel.id);
+ if (result.error) {
+ alertErrorWithFallback(
+ this.context.intl,
+ result.error,
+ {
+ id: t('mobile.channel_info.delete_failed'),
+ defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
+ },
+ {
+ displayName: channel.display_name.trim(),
+ },
+ );
+ if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
+ this.props.actions.getChannel(channel.id);
+ }
+ } else if (this.props.viewArchivedChannels) {
+ this.props.actions.handleSelectChannel(channel.id);
+ this.close(false);
+ } else {
+ this.props.actions.selectPenultimateChannel(channel.team_id);
+ this.close(false);
+ }
+ };
+ this.alertAndHandleYesAction(title, message, onPressAction);
+ });
+
+ handleUnarchive = preventDoubleTap(() => {
+ const channel = this.props.currentChannel;
+ const title = {id: t('mobile.channel_info.alertTitleUnarchiveChannel'), defaultMessage: 'Unarchive {term}'};
+ const message = {
+ id: t('mobile.channel_info.alertMessageUnarchiveChannel'),
+ defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
+ };
+ const onPressAction = async () => {
+ const result = await this.props.actions.unarchiveChannel(channel.id);
+ if (result.error) {
+ alertErrorWithFallback(
+ this.context.intl,
+ result.error,
+ {
+ id: t('mobile.channel_info.unarchive_failed'),
+ defaultMessage: "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
+ },
+ {
+ displayName: channel.display_name.trim(),
+ },
+ );
+ if (result.error.server_error_id === 'api.channel.unarchive_channel.unarchive.app_error') {
+ this.props.actions.getChannel(channel.id);
+ }
+ } else {
+ this.close(false);
+ }
+ };
+ this.alertAndHandleYesAction(title, message, onPressAction);
+ });
+
+ alertAndHandleYesAction = (title, message, onPressAction) => {
const {formatMessage} = this.context.intl;
const channel = this.props.currentChannel;
const term = channel.type === General.OPEN_CHANNEL ?
formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'});
- let title;
- let message;
- let onPressAction;
- if (eventType === 'leave') {
- title = {id: t('mobile.channel_info.alertTitleLeaveChannel'), defaultMessage: 'Leave {term}'};
- message = {
- id: t('mobile.channel_info.alertMessageLeaveChannel'),
- defaultMessage: 'Are you sure you want to leave the {term} {name}?',
- };
- onPressAction = () => {
- this.props.actions.leaveChannel(channel, true).then(() => {
- this.close();
- });
- };
- } else if (eventType === 'delete') {
- title = {id: t('mobile.channel_info.alertTitleDeleteChannel'), defaultMessage: 'Archive {term}'};
- message = {
- id: t('mobile.channel_info.alertMessageDeleteChannel'),
- defaultMessage: 'Are you sure you want to archive the {term} {name}?',
- };
- onPressAction = async () => {
- const result = await this.props.actions.deleteChannel(channel.id);
- if (result.error) {
- alertErrorWithFallback(
- this.context.intl,
- result.error,
- {
- id: t('mobile.channel_info.delete_failed'),
- defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
- },
- {
- displayName: channel.display_name.trim(),
- },
- );
- if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
- this.props.actions.getChannel(channel.id);
- }
- } else if (this.props.viewArchivedChannels) {
- this.props.actions.handleSelectChannel(channel.id);
- this.close(false);
- } else {
- this.props.actions.selectPenultimateChannel(channel.team_id);
- this.close(false);
- }
- };
- }
Alert.alert(
formatMessage(title, {term}),
@@ -303,7 +331,7 @@ export default class ChannelInfo extends PureComponent {
onPress: onPressAction,
}],
);
- });
+ }
handleClose = preventDoubleTap(() => {
const {currentChannel, isCurrent, isFavorite} = this.props;
@@ -410,6 +438,19 @@ export default class ChannelInfo extends PureComponent {
return isDirectMessage || isGroupMessage;
};
+ renderUnarchiveChannel = () => {
+ const {canUnarchiveChannel} = this.props;
+ if (!canUnarchiveChannel) {
+ return false;
+ }
+ const channel = this.props.currentChannel;
+ const channelIsArchived = channel.delete_at !== 0;
+ const isDirectMessage = channel.type === General.DM_CHANNEL;
+ const isGroupMessage = channel.type === General.GM_CHANNEL;
+
+ return channelIsArchived && (!isDirectMessage && !isGroupMessage);
+ };
+
renderConvertToPrivateRow = () => {
const {currentChannel, canConvertChannel} = this.props;
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
@@ -440,7 +481,6 @@ export default class ChannelInfo extends PureComponent {
theme={theme}
isLandscape={isLandscape}
/>
-
);
}
@@ -627,6 +667,19 @@ export default class ChannelInfo extends PureComponent {
theme={theme}
isLandscape={isLandscape}
/>
+ {this.renderUnarchiveChannel() &&
+
+
+
+
+ }
}
diff --git a/app/screens/channel_info/channel_info.test.js b/app/screens/channel_info/channel_info.test.js
index 42dc7d8b5..bad5f6a3c 100644
--- a/app/screens/channel_info/channel_info.test.js
+++ b/app/screens/channel_info/channel_info.test.js
@@ -36,6 +36,7 @@ describe('channel_info', () => {
};
const baseProps = {
canDeleteChannel: true,
+ canUnarchiveChannel: false,
canConvertChannel: true,
canManageUsers: true,
viewArchivedChannels: true,
@@ -70,6 +71,7 @@ describe('channel_info', () => {
closeGMChannel: jest.fn(),
convertChannelToPrivate: jest.fn(),
deleteChannel: jest.fn(),
+ unarchiveChannel: jest.fn(),
getChannelStats: jest.fn(),
getChannel: jest.fn(),
leaveChannel: jest.fn(),
@@ -166,4 +168,38 @@ describe('channel_info', () => {
instance.close();
expect(dismissModal).toHaveBeenCalled();
});
+
+ test('should render unarchive channel button when currentChannel is an archived channel', async () => {
+ const props = Object.assign({}, baseProps);
+ props.canUnarchiveChannel = true;
+ props.currentChannel.delete_at = 1234566;
+
+ const wrapper = shallow(
+ ,
+ {context: {intl: intlMock}},
+ );
+
+ const instance = wrapper.instance();
+ const render = instance.renderUnarchiveChannel();
+ expect(render).toBeTruthy();
+ });
+
+ test('should not render unarchive channel button when currentChannel is an active channel', async () => {
+ const props = Object.assign({}, baseProps);
+ props.canUnarchiveChannel = false;
+ props.currentChannel.delete_at = 0;
+
+ const wrapper = shallow(
+ ,
+ {context: {intl: intlMock}},
+ );
+
+ const instance = wrapper.instance();
+ const render = instance.renderUnarchiveChannel();
+ expect(render).toBeFalsy();
+ });
});
diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js
index ed82fb30e..a699f0547 100644
--- a/app/screens/channel_info/index.js
+++ b/app/screens/channel_info/index.js
@@ -10,6 +10,7 @@ import {
getChannelStats,
getChannel,
deleteChannel,
+ unarchiveChannel,
unfavoriteChannel,
updateChannelNotifyProps,
} from 'mattermost-redux/actions/channels';
@@ -29,9 +30,13 @@ import {
import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
-import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
+import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general';
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
+import Permissions from 'mattermost-redux/constants/permissions';
+import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
+import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {
closeDMChannel,
@@ -63,6 +68,7 @@ function mapStateToProps(state) {
const isCurrent = currentChannel.id === state.entities.channels.currentChannelId;
const isFavorite = favoriteChannels && favoriteChannels.indexOf(currentChannel.id) > -1;
const roles = getCurrentUserRoles(state);
+ const {serverVersion} = state.entities.general;
let canManageUsers = currentChannel.hasOwnProperty('id') ? canManageChannelMembers(state) : false;
if (currentChannel.group_constrained) {
canManageUsers = false;
@@ -100,8 +106,17 @@ function mapStateToProps(state) {
timeZone = getUserCurrentTimezone(currentUser.timezone);
}
+ let canUnarchiveChannel = false;
+ if (hasNewPermissions(state) && isMinimumServerVersion(serverVersion, 5, 20)) {
+ canUnarchiveChannel = haveITeamPermission(state, {
+ team: getCurrentTeamId(state),
+ permission: Permissions.MANAGE_TEAM,
+ });
+ }
+
return {
canDeleteChannel: showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin),
+ canUnarchiveChannel,
canConvertChannel: isAdmin,
viewArchivedChannels,
canEditChannel,
@@ -134,6 +149,7 @@ function mapDispatchToProps(dispatch) {
closeGMChannel,
convertChannelToPrivate,
deleteChannel,
+ unarchiveChannel,
getChannelStats,
getChannel,
leaveChannel,
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 111d5217f..491df5c26 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -159,10 +159,12 @@
"mobile.channel_info.alertMessageConvertChannel": "When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?",
"mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to archive the {term} {name}?",
"mobile.channel_info.alertMessageLeaveChannel": "Are you sure you want to leave the {term} {name}?",
+ "mobile.channel_info.alertMessageUnarchiveChannel": "Are you sure you want to unarchive the {term} {name}?",
"mobile.channel_info.alertNo": "No",
"mobile.channel_info.alertTitleConvertChannel": "Convert {displayName} to a private channel?",
"mobile.channel_info.alertTitleDeleteChannel": "Archive {term}",
"mobile.channel_info.alertTitleLeaveChannel": "Leave {term}",
+ "mobile.channel_info.alertTitleUnarchiveChannel": "Unarchive {term}",
"mobile.channel_info.alertYes": "Yes",
"mobile.channel_info.convert": "Convert to Private Channel",
"mobile.channel_info.convert_failed": "We were unable to convert {displayName} to a private channel.",
@@ -173,6 +175,7 @@
"mobile.channel_info.edit": "Edit Channel",
"mobile.channel_info.privateChannel": "Private Channel",
"mobile.channel_info.publicChannel": "Public Channel",
+ "mobile.channel_info.unarchive_failed": "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
"mobile.channel_list.alertNo": "No",
"mobile.channel_list.alertYes": "Yes",
"mobile.channel_list.archived": "ARCHIVED",
@@ -411,6 +414,7 @@
"mobile.routes.channelInfo.delete_channel": "Archive Channel",
"mobile.routes.channelInfo.favorite": "Favorite",
"mobile.routes.channelInfo.groupManaged": "Members are managed by linked groups",
+ "mobile.routes.channelInfo.unarchive_channel": "Unarchive Channel",
"mobile.routes.code": "{language} Code",
"mobile.routes.code.noLanguage": "Code",
"mobile.routes.edit_profile": "Edit Profile",
@@ -469,6 +473,7 @@
"mobile.storage_permission_denied_title": "{applicationName} would like to access your files",
"mobile.suggestion.members": "Members",
"mobile.system_message.channel_archived_message": "{username} archived the channel",
+ "mobile.system_message.channel_unarchived_message": "{username} unarchived the channel",
"mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}",
"mobile.system_message.update_channel_header_message_and_forget.removed": "{username} removed the channel header (was: {oldHeader})",
"mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} updated the channel header from: {oldHeader} to: {newHeader}",