From 525f451628a38af1bfe1e193c95b37e8c5201653 Mon Sep 17 00:00:00 2001 From: Lucas Sartor Chauvin Date: Wed, 5 May 2021 16:35:43 -0300 Subject: [PATCH] [NEW] Option to prevent users from using Invisible status (#20084) Co-authored-by: Diego Sampaio --- app/api/server/v1/users.js | 8 ++ app/lib/server/startup/settings.js | 6 + app/ui/client/lib/userPopoverStatus.js | 114 ++++++++++-------- .../server/methods/setUserStatus.js | 7 +- client/components/UserStatusMenu.js | 14 ++- client/sidebar/header/UserDropdown.js | 46 ++++--- packages/rocketchat-i18n/i18n/en.i18n.json | 3 +- packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 3 +- tests/end-to-end/api/01-users.js | 31 ++++- 9 files changed, 146 insertions(+), 86 deletions(-) diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index 7eb1a9006e0..a8f850541b7 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -456,12 +456,20 @@ API.v1.addRoute('users.setStatus', { authRequired: true }, { const validStatus = ['online', 'away', 'offline', 'busy']; if (validStatus.includes(this.bodyParams.status)) { const { status } = this.bodyParams; + + if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'users.setStatus', + }); + } + Meteor.users.update(user._id, { $set: { status, statusDefault: status, }, }); + setUserStatus(user, status); } else { throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js index 7f9a11c84cc..642ac5f31b4 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -118,6 +118,12 @@ settings.addGroup('Accounts', function() { ], public: true, }); + this.add('Accounts_AllowInvisibleStatusOption', true, { + type: 'boolean', + public: true, + i18nLabel: 'Accounts_AllowInvisibleStatusOption', + }); + this.section('Registration', function() { this.add('Accounts_Send_Email_When_Activating', true, { type: 'boolean', diff --git a/app/ui/client/lib/userPopoverStatus.js b/app/ui/client/lib/userPopoverStatus.js index 8d9720f5ce7..85193364701 100644 --- a/app/ui/client/lib/userPopoverStatus.js +++ b/app/ui/client/lib/userPopoverStatus.js @@ -1,57 +1,65 @@ import { t } from '../../../utils'; +import { settings } from '../../../settings/client'; -export const getPopoverStatusConfig = (currentTarget, actionCallback) => ({ - popoverClass: 'edit-status-type', - columns: [ +export const getPopoverStatusConfig = (currentTarget, actionCallback) => { + const items = [ { - groups: [ - { - items: [ - { - icon: 'circle', - name: t('Online'), - modifier: 'online', - action: () => { - (typeof actionCallback === 'function') && actionCallback('online'); - $('input[name=statusType]').val('online'); - $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--online'); - }, - }, - { - icon: 'circle', - name: t('Away'), - modifier: 'away', - action: () => { - (typeof actionCallback === 'function') && actionCallback('away'); - $('input[name=statusType]').val('away'); - $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--away'); - }, - }, - { - icon: 'circle', - name: t('Busy'), - modifier: 'busy', - action: () => { - (typeof actionCallback === 'function') && actionCallback('busy'); - $('input[name=statusType]').val('busy'); - $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--busy'); - }, - }, - { - icon: 'circle', - name: t('Invisible'), - modifier: 'offline', - action: () => { - (typeof actionCallback === 'function') && actionCallback('offline'); - $('input[name=statusType]').val('offline'); - $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--offline'); - }, - }, - ], - }, - ], + icon: 'circle', + name: t('Online'), + modifier: 'online', + action: () => { + (typeof actionCallback === 'function') && actionCallback('online'); + $('input[name=statusType]').val('online'); + $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--online'); + }, }, - ], - currentTarget, - offsetVertical: currentTarget.clientHeight, -}); + { + icon: 'circle', + name: t('Away'), + modifier: 'away', + action: () => { + (typeof actionCallback === 'function') && actionCallback('away'); + $('input[name=statusType]').val('away'); + $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--away'); + }, + }, + { + icon: 'circle', + name: t('Busy'), + modifier: 'busy', + action: () => { + (typeof actionCallback === 'function') && actionCallback('busy'); + $('input[name=statusType]').val('busy'); + $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--busy'); + }, + }, + ]; + + if (settings.get('Accounts_AllowInvisibleStatusOption')) { + items.push({ + icon: 'circle', + name: t('Invisible'), + modifier: 'offline', + action: () => { + (typeof actionCallback === 'function') && actionCallback('offline'); + $('input[name=statusType]').val('offline'); + $(currentTarget).prop('class', 'rc-input__icon js-status-type edit-status-type-icon--offline'); + }, + }); + } + + return { + popoverClass: 'edit-status-type', + columns: [ + { + groups: [ + { + items, + }, + ], + }, + ], + currentTarget, + offsetVertical: currentTarget.clientHeight, + }; +}; diff --git a/app/user-status/server/methods/setUserStatus.js b/app/user-status/server/methods/setUserStatus.js index adb4aed135f..769e1992678 100644 --- a/app/user-status/server/methods/setUserStatus.js +++ b/app/user-status/server/methods/setUserStatus.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { settings } from '../../../settings'; -import { RateLimiter, setStatusText } from '../../../lib'; +import { settings } from '../../../settings/server'; +import { RateLimiter, setStatusText } from '../../../lib/server'; Meteor.methods({ setUserStatus(statusType, statusText) { @@ -12,6 +12,9 @@ Meteor.methods({ } if (statusType) { + if (statusType === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { method: 'setUserStatus' }); + } Meteor.call('UserPresence:setDefaultStatus', statusType); } diff --git a/client/components/UserStatusMenu.js b/client/components/UserStatusMenu.js index 484018aa75c..c14b8f77a85 100644 --- a/client/components/UserStatusMenu.js +++ b/client/components/UserStatusMenu.js @@ -1,6 +1,7 @@ import { Button, PositionAnimated, Options, useCursor, Box } from '@rocket.chat/fuselage'; import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react'; +import { useSetting } from '../contexts/SettingsContext'; import { useTranslation } from '../contexts/TranslationContext'; import { UserStatus } from './UserStatus'; @@ -15,6 +16,8 @@ const UserStatusMenu = ({ const [status, setStatus] = useState(initialStatus); + const allowInvisibleStatus = useSetting('Accounts_AllowInvisibleStatusOption'); + const options = useMemo(() => { const renderOption = (status, label) => ( @@ -25,13 +28,18 @@ const UserStatusMenu = ({ ); - return [ + const statuses = [ ['online', renderOption('online', t('Online'))], ['busy', renderOption('busy', t('Busy'))], ['away', renderOption('away', t('Away'))], - ['offline', renderOption('offline', t('Invisible'))], ]; - }, [t]); + + if (allowInvisibleStatus) { + statuses.push(['offline', renderOption('offline', t('Invisible'))]); + } + + return statuses; + }, [t, allowInvisibleStatus]); const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] = useCursor( -1, diff --git a/client/sidebar/header/UserDropdown.js b/client/sidebar/header/UserDropdown.js index 2b3fb2dd9a9..497320a1724 100644 --- a/client/sidebar/header/UserDropdown.js +++ b/client/sidebar/header/UserDropdown.js @@ -54,6 +54,10 @@ const UserDropdown = ({ user, onClose }) => { const { name, username, avatarETag, status, statusText } = user; const useRealName = useSetting('UI_Use_Real_Name'); + const filterInvisibleStatus = !useSetting('Accounts_AllowInvisibleStatusOption') + ? (key) => userStatus.list[key].name !== 'invisible' + : () => true; + const showAdmin = useAtLeastOnePermission(ADMIN_PERMISSIONS); const handleCustomStatus = useMutableCallback((e) => { @@ -131,26 +135,28 @@ const UserDropdown = ({ user, onClose }) => { {t('Status')} - {Object.keys(userStatus.list).map((key, i) => { - const status = userStatus.list[key]; - const name = status.localizeName ? t(status.name) : status.name; - const modifier = status.statusType || user.status; - - return ( - - ); - })} + {Object.keys(userStatus.list) + .filter(filterInvisibleStatus) + .map((key, i) => { + const status = userStatus.list[key]; + const name = status.localizeName ? t(status.name) : status.name; + const modifier = status.statusType || user.status; + + return ( + + ); + })}