[MM-20804] Add "Unarchive Channel" option to the channel info screen (#3663)

* undelete

* fix build

* change let to const

* ran make i18n

* small refactor

* using canUndelete var

* added render message

* fix bug

* sync

* remove duplicate

* fix test

* add unit test for unarchive channel button render

* fix unit test for unarchive channel

* delete space

* change logic in undelete

* add server minimum for feature

* fix renderunarchive test with adding props variables

* snpashot wasnt matching. fixed by adding to base prop

* return early if false

* change position and color for unarchive button

* refactor handle delete undelete leave

* move serverversion call up

* undelete to unarchive

* remove unnecessary seperator

* hash

* Update app/screens/channel_info/index.js

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* delete canuseunarchive feature

* add test to render sys message

* remote 'w' in package.json

* add trailing comma

* update redux hash

* whitespace in test

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Allen Lai 2020-02-26 16:02:28 -08:00 committed by GitHub
parent 30594cec5d
commit 3e2fc1bc67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 204 additions and 58 deletions

View file

@ -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`] = `
<Connect(Markdown)
baseTextStyle={Object {}}
onPostPress={[MockFunction]}
textStyles={Object {}}
value="{username} unarchived the channel"
/>
`;

View file

@ -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) => {

View file

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

View file

@ -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}
/>
<View style={style.separator}/>
</View>);
}
@ -627,6 +667,19 @@ export default class ChannelInfo extends PureComponent {
theme={theme}
isLandscape={isLandscape}
/>
{this.renderUnarchiveChannel() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleUnarchive}
defaultMessage='Unarchive Channel'
icon='archive' // might need to change the icon...
textId={t('mobile.routes.channelInfo.unarchive_channel')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
</React.Fragment>
}
</View>

View file

@ -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(
<ChannelInfo
{...props}
/>,
{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(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderUnarchiveChannel();
expect(render).toBeFalsy();
});
});

View file

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

View file

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