diff --git a/src/CONST.js b/src/CONST.js index a04c20192037..c296dbc1e0ed 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -579,6 +579,11 @@ const CONST = { DAILY: 'daily', ALWAYS: 'always', }, + // Options for which room members can post + WRITE_CAPABILITIES: { + ALL: 'all', + ADMINS: 'admins', + }, VISIBILITY: { PUBLIC: 'public', PUBLIC_ANNOUNCE: 'public_announce', diff --git a/src/ROUTES.js b/src/ROUTES.js index 2dc2338f2e05..6fd4f7376b73 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -126,9 +126,11 @@ export default { REPORT_SETTINGS: 'r/:reportID/settings', REPORT_SETTINGS_ROOM_NAME: 'r/:reportID/settings/room-name', REPORT_SETTINGS_NOTIFICATION_PREFERENCES: 'r/:reportID/settings/notification-preferences', + REPORT_SETTINGS_WRITE_CAPABILITY: 'r/:reportID/settings/who-can-post', getReportSettingsRoute: (reportID) => `r/${reportID}/settings`, getReportSettingsRoomNameRoute: (reportID) => `r/${reportID}/settings/room-name`, getReportSettingsNotificationPreferencesRoute: (reportID) => `r/${reportID}/settings/notification-preferences`, + getReportSettingsWriteCapabilityRoute: (reportID) => `r/${reportID}/settings/who-can-post`, TRANSITION_FROM_OLD_DOT: 'transition', VALIDATE_LOGIN: 'v/:accountID/:validateCode', GET_ASSISTANCE: 'get-assistance/:taskID', diff --git a/src/languages/en.js b/src/languages/en.js index 9f4426a2a441..15b6f7112709 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -300,6 +300,13 @@ export default { `This workspace chat is no longer active because ${displayName} is no longer a member of the ${policyName} workspace.`, [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}) => `This workspace chat is no longer active because ${policyName} is no longer an active workspace.`, }, + writeCapabilityPage: { + label: 'Who can post', + writeCapability: { + all: 'All members', + admins: 'Admins only', + }, + }, sidebarScreen: { fabAction: 'New chat', newChat: 'New chat', diff --git a/src/languages/es.js b/src/languages/es.js index 8a024d2f3e8a..11807af9892e 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -299,6 +299,13 @@ export default { `Este chat de espacio de trabajo esta desactivado porque ${displayName} ha dejado de ser miembro del espacio de trabajo ${policyName}.`, [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}) => `Este chat de espacio de trabajo esta desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`, }, + writeCapabilityPage: { + label: 'QuiƩn puede postear', + writeCapability: { + all: 'Todos los miembros', + admins: 'Solo administradores', + }, + }, sidebarScreen: { fabAction: 'Nuevo chat', newChat: 'Nuevo chat', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index de821060b832..d8a6d8a94f63 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -191,6 +191,13 @@ const ReportSettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Report_Settings_Notification_Preferences', }, + { + getComponent: () => { + const WriteCapabilityPage = require('../../../pages/settings/Report/WriteCapabilityPage').default; + return WriteCapabilityPage; + }, + name: 'Report_Settings_Write_Capability', + }, ]); const TaskModalStackNavigator = createModalStackNavigator([ diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 1535880e8997..c071f52ad092 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -224,6 +224,9 @@ export default { Report_Settings_Notification_Preferences: { path: ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES, }, + Report_Settings_Write_Capability: { + path: ROUTES.REPORT_SETTINGS_WRITE_CAPABILITY, + }, }, }, NewGroup: { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 6122701476f7..835330ca3ca0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -432,6 +432,26 @@ function getPolicyName(report) { return policy.name || report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); } +/** + * Checks if the current user is allowed to comment on the given report. + * @param {Object} report + * @param {String} [report.writeCapability] + * @returns {Boolean} + */ +function isAllowedToComment(report) { + // Default to allowing all users to post + const capability = lodashGet(report, 'writeCapability', CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; + + if (capability === CONST.REPORT.WRITE_CAPABILITIES.ALL) { + return true; + } + + // If we've made it here, commenting on this report is restricted. + // If the user is an admin, allow them to post. + const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + return lodashGet(policy, 'role', '') === CONST.POLICY.ROLE.ADMIN; +} + /** * Checks if the current user is the admin of the policy given the policy expense chat. * @param {Object} report @@ -2303,5 +2323,6 @@ export { shouldReportShowSubscript, isReportDataReady, isSettled, + isAllowedToComment, getMoneyRequestAction, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 6a4cd9fe3050..42e3b3cfc1f4 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1118,6 +1118,35 @@ function updateNotificationPreferenceAndNavigate(reportID, previousValue, newVal Navigation.drawerGoBack(ROUTES.getReportSettingsRoute(reportID)); } +/** + * @param {Object} report + * @param {String} newValue + */ +function updateWriteCapabilityAndNavigate(report, newValue) { + if (report.writeCapability === newValue) { + Navigation.drawerGoBack(ROUTES.getReportSettingsRoute(report.reportID)); + return; + } + + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: {writeCapability: newValue}, + }, + ]; + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: {writeCapability: report.writeCapability}, + }, + ]; + API.write('UpdateReportWriteCapability', {reportID: report.reportID, writeCapability: newValue}, {optimisticData, failureData}); + // Return to the report settings page since this field utilizes push-to-page + Navigation.drawerGoBack(ROUTES.getReportSettingsRoute(report.reportID)); +} + /** * Navigates to the 1:1 report with Concierge */ @@ -1639,6 +1668,7 @@ export { addComment, addAttachment, reconnect, + updateWriteCapabilityAndNavigate, updateNotificationPreferenceAndNavigate, subscribeToReportTypingEvents, unsubscribeFromReportChannel, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 5ae3b0e8e294..223606dc30c3 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -359,6 +359,7 @@ class ReportScreen extends React.Component { report={this.props.report} isComposerFullSize={this.props.isComposerFullSize} onSubmitComment={this.onSubmitComment} + policies={this.props.policies} /> )} diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 92811fa7b0fc..cdaad9b5ffc2 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -65,7 +65,8 @@ class ReportFooter extends React.Component { render() { const isArchivedRoom = ReportUtils.isArchivedRoom(this.props.report); - const hideComposer = isArchivedRoom || !_.isEmpty(this.props.errors); + const isAllowedToComment = ReportUtils.isAllowedToComment(this.props.report); + const hideComposer = isArchivedRoom || !_.isEmpty(this.props.errors) || !isAllowedToComment; return ( <> diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 17757480bbe7..bb7c197e8bf2 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -67,4 +67,7 @@ export default PropTypes.shape({ /** The status of the current report */ statusNum: PropTypes.oneOf(_.values(CONST.REPORT.STATUS)), + + /** Which user role is capable of posting messages on the report */ + writeCapability: PropTypes.oneOf(_.values(CONST.REPORT.WRITE_CAPABILITIES)), }); diff --git a/src/pages/settings/Report/ReportSettingsPage.js b/src/pages/settings/Report/ReportSettingsPage.js index 07ccf07d4db3..6e3b460411de 100644 --- a/src/pages/settings/Report/ReportSettingsPage.js +++ b/src/pages/settings/Report/ReportSettingsPage.js @@ -84,6 +84,9 @@ class ReportSettingsPage extends Component { const linkedWorkspace = _.find(this.props.policies, (policy) => policy && policy.id === this.props.report.policyID); const shouldDisableRename = this.shouldDisableRename(linkedWorkspace) || ReportUtils.isThread(this.props.report); const notificationPreference = this.props.translate(`notificationPreferencesPage.notificationPreferences.${this.props.report.notificationPreference}`); + const writeCapability = this.props.report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL; + const writeCapabilityText = this.props.translate(`writeCapabilityPage.writeCapability.${writeCapability}`); + const shouldAllowWriteCapabilityEditing = lodashGet(linkedWorkspace, 'role', '') === CONST.POLICY.ROLE.ADMIN; return ( @@ -131,6 +134,29 @@ class ReportSettingsPage extends Component { )} )} + {shouldAllowWriteCapabilityEditing ? ( + Navigation.navigate(ROUTES.getReportSettingsWriteCapabilityRoute(this.props.report.reportID))} + /> + ) : ( + + + {this.props.translate('writeCapabilityPage.label')} + + + {writeCapabilityText} + + + )} {Boolean(linkedWorkspace) && ( diff --git a/src/pages/settings/Report/WriteCapabilityPage.js b/src/pages/settings/Report/WriteCapabilityPage.js new file mode 100644 index 000000000000..40246f29aadc --- /dev/null +++ b/src/pages/settings/Report/WriteCapabilityPage.js @@ -0,0 +1,67 @@ +import React from 'react'; +import _ from 'underscore'; +import CONST from '../../../CONST'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import styles from '../../../styles/styles'; +import OptionsList from '../../../components/OptionsList'; +import Navigation from '../../../libs/Navigation/Navigation'; +import compose from '../../../libs/compose'; +import withReportOrNotFound from '../../home/report/withReportOrNotFound'; +import reportPropTypes from '../../reportPropTypes'; +import ROUTES from '../../../ROUTES'; +import * as Report from '../../../libs/actions/Report'; +import * as Expensicons from '../../../components/Icon/Expensicons'; +import themeColors from '../../../styles/themes/default'; + +const propTypes = { + ...withLocalizePropTypes, + + /** The report for which we are setting write capability */ + report: reportPropTypes.isRequired, +}; +const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success}; + +const WriteCapabilityPage = (props) => { + const writeCapabilityOptions = _.map(CONST.REPORT.WRITE_CAPABILITIES, (value) => ({ + value, + text: props.translate(`writeCapabilityPage.writeCapability.${value}`), + keyForList: value, + + // Include the green checkmark icon to indicate the currently selected value + customIcon: value === (props.report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL) ? greenCheckmark : null, + + // This property will make the currently selected value have bold text + boldStyle: value === (props.report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL), + })); + + return ( + + Navigation.navigate(ROUTES.getReportSettingsRoute(props.report.reportID))} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + Report.updateWriteCapabilityAndNavigate(props.report, option.value)} + hideSectionHeaders + optionHoveredStyle={{ + ...styles.hoveredComponentBG, + ...styles.mhn5, + ...styles.ph5, + }} + shouldHaveOptionSeparator + shouldDisableRowInnerPadding + contentContainerStyles={[styles.ph5]} + /> + + ); +}; + +WriteCapabilityPage.displayName = 'WriteCapabilityPage'; +WriteCapabilityPage.propTypes = propTypes; + +export default compose(withLocalize, withReportOrNotFound)(WriteCapabilityPage);