[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:
parent
30594cec5d
commit
3e2fc1bc67
7 changed files with 204 additions and 58 deletions
|
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue