[NEW] Custom User Status (#13933)

Co-Authored-By: Tasso Evangelista <tasso@tassoevan.me>
Co-Authored-By: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-Authored-By: wreiske <wreiske@mieweb.com>
pull/14852/head
Pierre H. Lehnen 6 years ago committed by Diego Sampaio
parent b954bb8d3c
commit fbb47b6783
  1. 2
      app/api/server/v1/emoji-custom.js
  2. 72
      app/api/server/v1/users.js
  3. 1
      app/authorization/server/startup.js
  4. 4
      app/custom-sounds/server/methods/deleteCustomSound.js
  5. 2
      app/custom-sounds/server/methods/insertOrUpdateSound.js
  6. 4
      app/emoji-custom/server/methods/deleteEmojiCustom.js
  7. 10
      app/lib/lib/roomTypes/direct.js
  8. 1
      app/lib/server/functions/getFullUserData.js
  9. 1
      app/lib/server/functions/index.js
  10. 13
      app/lib/server/functions/saveUser.js
  11. 45
      app/lib/server/functions/setStatusMessage.js
  12. 4
      app/lib/server/startup/settings.js
  13. 2
      app/models/client/index.js
  14. 10
      app/models/client/models/CustomUserStatus.js
  15. 2
      app/models/server/index.js
  16. 6
      app/models/server/models/CustomSounds.js
  17. 66
      app/models/server/models/CustomUserStatus.js
  18. 4
      app/models/server/models/EmojiCustom.js
  19. 11
      app/models/server/models/Users.js
  20. 11
      app/slashcommands-join/server/server.js
  21. 1
      app/slashcommands-status/client/index.js
  22. 8
      app/slashcommands-status/index.js
  23. 46
      app/slashcommands-status/lib/status.js
  24. 1
      app/slashcommands-status/server/index.js
  25. 4
      app/slashcommands-topic/lib/topic.js
  26. 9
      app/theme/client/imports/components/header.css
  27. 24
      app/theme/client/imports/components/popover.css
  28. 77
      app/theme/client/imports/general/base_old.css
  29. 20
      app/ui-account/client/accountProfile.html
  30. 31
      app/ui-account/client/accountProfile.js
  31. 14
      app/ui-flextab/client/tabs/userEdit.html
  32. 1
      app/ui-flextab/client/tabs/userEdit.js
  33. 2
      app/ui-flextab/client/tabs/userInfo.html
  34. 12
      app/ui-flextab/client/tabs/userInfo.js
  35. 5
      app/ui-master/client/main.js
  36. 79
      app/ui-sidenav/client/sidebarHeader.js
  37. 4
      app/ui-utils/client/lib/AccountBox.js
  38. 9
      app/ui-utils/client/lib/popover.html
  39. 2
      app/ui/client/components/header/headerRoom.html
  40. 17
      app/ui/client/components/header/headerRoom.js
  41. 23
      app/ui/client/components/selectDropdown.html
  42. 23
      app/ui/client/components/selectDropdown.js
  43. 5
      app/ui/client/index.js
  44. 1
      app/ui/client/lib/iframeCommands.js
  45. 45
      app/ui/client/views/app/editStatus.css
  46. 30
      app/ui/client/views/app/editStatus.html
  47. 113
      app/ui/client/views/app/editStatus.js
  48. 10
      app/ui/client/views/app/room.js
  49. 72
      app/user-status/client/admin/adminUserStatus.html
  50. 137
      app/user-status/client/admin/adminUserStatus.js
  51. 7
      app/user-status/client/admin/adminUserStatusEdit.html
  52. 7
      app/user-status/client/admin/adminUserStatusInfo.html
  53. 9
      app/user-status/client/admin/route.js
  54. 11
      app/user-status/client/admin/startup.js
  55. 42
      app/user-status/client/admin/userStatusEdit.html
  56. 115
      app/user-status/client/admin/userStatusEdit.js
  57. 22
      app/user-status/client/admin/userStatusInfo.html
  58. 117
      app/user-status/client/admin/userStatusInfo.js
  59. 5
      app/user-status/client/admin/userStatusPreview.html
  60. 17
      app/user-status/client/index.js
  61. 54
      app/user-status/client/lib/customUserStatus.js
  62. 36
      app/user-status/client/lib/userStatus.js
  63. 8
      app/user-status/client/notifications/deleteCustomUserStatus.js
  64. 8
      app/user-status/client/notifications/updateCustomUserStatus.js
  65. 8
      app/user-status/index.js
  66. 6
      app/user-status/server/index.js
  67. 26
      app/user-status/server/methods/deleteCustomUserStatus.js
  68. 70
      app/user-status/server/methods/insertOrUpdateUserStatus.js
  69. 9
      app/user-status/server/methods/listCustomUserStatus.js
  70. 30
      app/user-status/server/methods/setUserStatus.js
  71. 30
      app/user-status/server/publications/fullUserStatusData.js
  72. 4
      app/utils/client/lib/roomTypes.js
  73. 1
      app/utils/server/functions/getDefaultUserFields.js
  74. 1
      client/importPackages.js
  75. 4
      client/startup/usersObserve.js
  76. 1
      imports/startup/client/listenActiveUsers.js
  77. 21
      packages/rocketchat-i18n/i18n/en.i18n.json
  78. 8
      packages/rocketchat-i18n/i18n/es.i18n.json
  79. 1
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  80. 2
      server/importPackages.js
  81. 4
      server/methods/saveUserProfile.js
  82. 1
      server/publications/activeUsers.js
  83. 46
      tests/end-to-end/api/01-users.js
  84. 4
      tests/pageobjects/administration.page.js

@ -122,7 +122,7 @@ API.v1.addRoute('emoji-custom.update', { authRequired: true }, {
if (!fields._id) {
return callback(new Meteor.Error('The required "_id" query param is missing.'));
}
const emojiToUpdate = EmojiCustom.findOneByID(fields._id);
const emojiToUpdate = EmojiCustom.findOneById(fields._id);
if (!emojiToUpdate) {
return callback(new Meteor.Error('Emoji not found.'));
}

@ -18,6 +18,7 @@ import {
} from '../../../lib';
import { getFullUserData } from '../../../lib/server/functions/getFullUserData';
import { API } from '../api';
import { setStatusMessage } from '../../../lib/server';
API.v1.addRoute('users.create', { authRequired: true }, {
post() {
@ -325,6 +326,73 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, {
},
});
API.v1.addRoute('users.getStatus', { authRequired: true }, {
get() {
if (this.isUserFromParams()) {
const user = Users.findOneById(this.userId);
return API.v1.success({
message: user.statusText,
connectionStatus: user.statusConnection,
status: user.status,
});
}
const user = this.getUserFromParams();
return API.v1.success({
message: user.statusText,
status: user.status,
});
},
});
API.v1.addRoute('users.setStatus', { authRequired: true }, {
post() {
check(this.bodyParams, Match.ObjectIncluding({
status: Match.Maybe(String),
message: Match.Maybe(String),
}));
if (!settings.get('Accounts_AllowUserStatusMessageChange')) {
throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', {
method: 'users.setStatus',
});
}
let user;
if (this.isUserFromParams()) {
user = Meteor.users.findOne(this.userId);
} else if (hasPermission(this.userId, 'edit-other-user-info')) {
user = this.getUserFromParams();
} else {
return API.v1.unauthorized();
}
Meteor.runAsUser(user._id, () => {
if (this.bodyParams.message) {
setStatusMessage(user._id, this.bodyParams.message);
}
if (this.bodyParams.status) {
const validStatus = ['online', 'away', 'offline', 'busy'];
if (validStatus.includes(this.bodyParams.status)) {
Meteor.users.update(this.userId, {
$set: {
status: this.bodyParams.status,
statusDefault: this.bodyParams.status,
},
});
} else {
throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', {
method: 'users.setStatus',
});
}
}
});
return API.v1.success();
},
});
API.v1.addRoute('users.update', { authRequired: true }, {
post() {
check(this.bodyParams, {
@ -334,6 +402,7 @@ API.v1.addRoute('users.update', { authRequired: true }, {
name: Match.Maybe(String),
password: Match.Maybe(String),
username: Match.Maybe(String),
statusText: Match.Maybe(String),
active: Match.Maybe(Boolean),
roles: Match.Maybe(Array),
joinDefaultChannels: Match.Maybe(Boolean),
@ -369,6 +438,7 @@ API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, {
email: Match.Maybe(String),
name: Match.Maybe(String),
username: Match.Maybe(String),
statusText: Match.Maybe(String),
currentPassword: Match.Maybe(String),
newPassword: Match.Maybe(String),
}),
@ -379,6 +449,7 @@ API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, {
email: this.bodyParams.data.email,
realname: this.bodyParams.data.name,
username: this.bodyParams.data.username,
statusText: this.bodyParams.data.statusText,
newPassword: this.bodyParams.data.newPassword,
typedPassword: this.bodyParams.data.currentPassword,
};
@ -581,6 +652,7 @@ API.v1.addRoute('users.presence', { authRequired: true }, {
name: 1,
status: 1,
utcOffset: 1,
statusText: 1,
},
};

@ -46,6 +46,7 @@ Meteor.startup(function() {
{ _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous'] },
{ _id: 'manage-assets', roles: ['admin'] },
{ _id: 'manage-emoji', roles: ['admin'] },
{ _id: 'manage-user-status', roles: ['admin'] },
{ _id: 'manage-integrations', roles: ['admin'] },
{ _id: 'manage-own-integrations', roles: ['admin'] },
{ _id: 'manage-oauth-apps', roles: ['admin'] },

@ -10,7 +10,7 @@ Meteor.methods({
let sound = null;
if (hasPermission(this.userId, 'manage-sounds')) {
sound = CustomSounds.findOneByID(_id);
sound = CustomSounds.findOneById(_id);
} else {
throw new Meteor.Error('not_authorized');
}
@ -20,7 +20,7 @@ Meteor.methods({
}
RocketChatFileCustomSoundsInstance.deleteFile(`${ sound._id }.${ sound.extension }`);
CustomSounds.removeByID(_id);
CustomSounds.removeById(_id);
Notifications.notifyAll('deleteCustomSound', { soundData: sound });
return true;

@ -32,7 +32,7 @@ Meteor.methods({
let matchingResults = [];
if (soundData._id) {
matchingResults = CustomSounds.findByNameExceptID(soundData.name, soundData._id).fetch();
matchingResults = CustomSounds.findByNameExceptId(soundData.name, soundData._id).fetch();
} else {
matchingResults = CustomSounds.findByName(soundData.name).fetch();
}

@ -10,7 +10,7 @@ Meteor.methods({
let emoji = null;
if (hasPermission(this.userId, 'manage-emoji')) {
emoji = EmojiCustom.findOneByID(emojiID);
emoji = EmojiCustom.findOneById(emojiID);
} else {
throw new Meteor.Error('not_authorized');
}
@ -20,7 +20,7 @@ Meteor.methods({
}
RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emoji.name }.${ emoji.extension }`));
EmojiCustom.removeByID(emojiID);
EmojiCustom.removeById(emojiID);
Notifications.notifyLogged('deleteEmojiCustom', { emojiData: emoji });
return true;

@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { ChatRoom, Subscriptions } from '../../../models';
import { ChatRoom, Subscriptions, Users } from '../../../models';
import { openRoom } from '../../../ui-utils';
import { getUserPreference, RoomTypeConfig, RoomTypeRouteConfig, RoomSettingsEnum, UiTextContext } from '../../../utils';
import { hasPermission, hasAtLeastOnePermission } from '../../../authorization';
@ -92,6 +92,14 @@ export class DirectMessageRoomType extends RoomTypeConfig {
return Session.get(`user_${ subscription.name }_status`);
}
getUserStatusText(roomId) {
const userId = roomId.replace(Meteor.userId(), '');
const userData = Users.findOne({ _id: userId });
if (userData && userData.statusText) {
return userData.statusText;
}
}
getDisplayName(room) {
return room.usernames.join(' x ');
}

@ -15,6 +15,7 @@ const defaultFields = {
type: 1,
active: 1,
reason: 1,
statusText: 1,
};
const fullFields = {

@ -23,6 +23,7 @@ export { saveUser } from './saveUser';
export { sendMessage } from './sendMessage';
export { setEmail } from './setEmail';
export { setRealName, _setRealName } from './setRealName';
export { setStatusMessage, _setStatusMessage } from './setStatusMessage';
export { setUserAvatar } from './setUserAvatar';
export { _setUsername, setUsername } from './setUsername';
export { unarchiveRoom } from './unarchiveRoom';

@ -10,7 +10,7 @@ import { settings } from '../../../settings';
import PasswordPolicy from '../lib/PasswordPolicyClass';
import { validateEmailDomain } from '../lib';
import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setRealName, setUsername } from '.';
import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setRealName, setUsername, setStatusMessage } from '.';
const passwordPolicy = new PasswordPolicy();
@ -133,6 +133,13 @@ function validateUserEditing(userId, userData) {
});
}
if (userData.statusText && !settings.get('Accounts_AllowUserStatusMessageChange') && (!canEditOtherUserInfo || editingMyself)) {
throw new Meteor.Error('error-action-not-allowed', 'Edit user status is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (userData.name && !settings.get('Accounts_AllowRealNameChange') && (!canEditOtherUserInfo || editingMyself)) {
throw new Meteor.Error('error-action-not-allowed', 'Edit user real name is not allowed', {
method: 'insertOrUpdateUser',
@ -248,6 +255,10 @@ export const saveUser = function(userId, userData) {
setRealName(userData._id, userData.name);
}
if (typeof userData.statusText === 'string') {
setStatusMessage(userData._id, userData.statusText);
}
if (userData.email) {
const shouldSendVerificationEmailToUser = userData.verified !== true;
setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser);

@ -0,0 +1,45 @@
import { Meteor } from 'meteor/meteor';
import s from 'underscore.string';
import { Users } from '../../../models';
import { Notifications } from '../../../notifications';
import { hasPermission } from '../../../authorization';
import { RateLimiter } from '../lib';
export const _setStatusMessage = function(userId, statusMessage) {
statusMessage = s.trim(statusMessage);
if (statusMessage.length > 120) {
statusMessage = statusMessage.substr(0, 120);
}
if (!userId) {
return false;
}
const user = Users.findOneById(userId);
// User already has desired statusMessage, return
if (user.statusText === statusMessage) {
return user;
}
// Set new statusMessage
Users.updateStatusText(user._id, statusMessage);
user.statusText = statusMessage;
Notifications.notifyLogged('Users:StatusMessageChanged', {
_id: user._id,
name: user.name,
username: user.username,
statusText: user.statusText,
});
return true;
};
export const setStatusMessage = RateLimiter.limitFunction(_setStatusMessage, 1, 60000, {
0() {
// Administrators have permission to change others status, so don't limit those
return !Meteor.userId() || !hasPermission(Meteor.userId(), 'edit-other-user-info');
},
});

@ -44,6 +44,10 @@ settings.addGroup('Accounts', function() {
type: 'boolean',
public: true,
});
this.add('Accounts_AllowUserStatusMessageChange', true, {
type: 'boolean',
public: true,
});
this.add('Accounts_AllowUsernameChange', true, {
type: 'boolean',
public: true,

@ -21,6 +21,7 @@ import { UserRoles } from './models/UserRoles';
import { AuthzCachedCollection, ChatPermissions } from './models/ChatPermissions';
import { WebdavAccounts } from './models/WebdavAccounts';
import CustomSounds from './models/CustomSounds';
import CustomUserStatus from './models/CustomUserStatus';
import EmojiCustom from './models/EmojiCustom';
const Users = _.extend({}, users, Meteor.users);
@ -51,6 +52,7 @@ export {
ChatSubscription,
Rooms,
CustomSounds,
CustomUserStatus,
EmojiCustom,
WebdavAccounts,
};

@ -0,0 +1,10 @@
import { Base } from './_Base';
class CustomUserStatus extends Base {
constructor() {
super();
this._initModel('custom_user_status');
}
}
export default new CustomUserStatus();

@ -15,6 +15,7 @@ import Statistics from './models/Statistics';
import Permissions from './models/Permissions';
import Roles from './models/Roles';
import CustomSounds from './models/CustomSounds';
import CustomUserStatus from './models/CustomUserStatus';
import Integrations from './models/Integrations';
import IntegrationHistory from './models/IntegrationHistory';
import CredentialTokens from './models/CredentialTokens';
@ -58,6 +59,7 @@ export {
Permissions,
Roles,
CustomSounds,
CustomUserStatus,
Integrations,
IntegrationHistory,
CredentialTokens,

@ -8,7 +8,7 @@ class CustomSounds extends Base {
}
// find one
findOneByID(_id, options) {
findOneById(_id, options) {
return this.findOne(_id, options);
}
@ -21,7 +21,7 @@ class CustomSounds extends Base {
return this.find(query, options);
}
findByNameExceptID(name, except, options) {
findByNameExceptId(name, except, options) {
const query = {
_id: { $nin: [except] },
name,
@ -48,7 +48,7 @@ class CustomSounds extends Base {
// REMOVE
removeByID(_id) {
removeById(_id) {
return this.remove(_id);
}
}

@ -0,0 +1,66 @@
import { Base } from './_Base';
class CustomUserStatus extends Base {
constructor() {
super('custom_user_status');
this.tryEnsureIndex({ name: 1 });
}
// find one
findOneById(_id, options) {
return this.findOne(_id, options);
}
// find
findByName(name, options) {
const query = {
name,
};
return this.find(query, options);
}
findByNameExceptId(name, except, options) {
const query = {
_id: { $nin: [except] },
name,
};
return this.find(query, options);
}
// update
setName(_id, name) {
const update = {
$set: {
name,
},
};
return this.update({ _id }, update);
}
setStatusType(_id, statusType) {
const update = {
$set: {
statusType,
},
};
return this.update({ _id }, update);
}
// INSERT
create(data) {
return this.insert(data);
}
// REMOVE
removeById(_id) {
return this.remove(_id);
}
}
export default new CustomUserStatus();

@ -10,7 +10,7 @@ class EmojiCustom extends Base {
}
// find one
findOneByID(_id, options) {
findOneById(_id, options) {
return this.findOne(_id, options);
}
@ -83,7 +83,7 @@ class EmojiCustom extends Base {
// REMOVE
removeByID(_id) {
removeById(_id) {
return this.remove(_id);
}
}

@ -15,6 +15,7 @@ export class Users extends Base {
this.tryEnsureIndex({ name: 1 });
this.tryEnsureIndex({ lastLogin: 1 });
this.tryEnsureIndex({ status: 1 });
this.tryEnsureIndex({ statusText: 1 });
this.tryEnsureIndex({ active: 1 }, { sparse: 1 });
this.tryEnsureIndex({ statusConnection: 1 }, { sparse: 1 });
this.tryEnsureIndex({ type: 1 });
@ -682,6 +683,16 @@ export class Users extends Base {
return this.update(query, update);
}
updateStatusText(_id, statusText) {
const update = {
$set: {
statusText,
},
};
return this.update(_id, update);
}
updateLastLoginById(_id) {
const update = {
$set: {

@ -1,8 +1,3 @@
/*
* Join is a named function that will replace /join commands
* @param {Object} message - The message object
*/
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Random } from 'meteor/random';
@ -12,7 +7,7 @@ import { Rooms, Subscriptions } from '../../models';
import { Notifications } from '../../notifications';
import { slashCommands } from '../../utils';
slashCommands.add('join', function Join(command, params, item) {
function Join(command, params, item) {
if (command !== 'join' || !Match.test(params, String)) {
return;
}
@ -42,7 +37,9 @@ slashCommands.add('join', function Join(command, params, item) {
});
}
Meteor.call('joinRoom', room._id);
}, {
}
slashCommands.add('join', Join, {
description: 'Join_the_given_channel',
params: '#channel',
});

@ -0,0 +1 @@
import '../lib/status';

@ -0,0 +1,8 @@
import { Meteor } from 'meteor/meteor';
if (Meteor.isClient) {
module.exports = require('./client/index.js');
}
if (Meteor.isServer) {
module.exports = require('./server/index.js');
}

@ -0,0 +1,46 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/tap:i18n';
import { Random } from 'meteor/random';
import { handleError, slashCommands } from '../../utils';
import { hasPermission } from '../../authorization';
import { Notifications } from '../../notifications';
function Status(command, params, item) {
if (command === 'status') {
if ((Meteor.isClient && hasPermission('edit-other-user-info')) || (Meteor.isServer && hasPermission(Meteor.userId(), 'edit-other-user-info'))) {
const user = Meteor.users.findOne(Meteor.userId());
Meteor.call('setUserStatus', null, params, (err) => {
if (err) {
if (Meteor.isClient) {
return handleError(err);
}
if (err.error === 'error-not-allowed') {
Notifications.notifyUser(Meteor.userId(), 'message', {
_id: Random.id(),
rid: item.rid,
ts: new Date(),
msg: TAPi18n.__('StatusMessage_Change_Disabled', null, user.language),
});
}
throw err;
} else {
Notifications.notifyUser(Meteor.userId(), 'message', {
_id: Random.id(),
rid: item.rid,
ts: new Date(),
msg: TAPi18n.__('StatusMessage_Changed_Successfully', null, user.language),
});
}
});
}
}
}
slashCommands.add('status', Status, {
description: 'Slash_Status_Description',
params: 'Slash_Status_Params',
});

@ -0,0 +1 @@
import '../lib/status';

@ -4,10 +4,6 @@ import { handleError, slashCommands } from '../../utils';
import { ChatRoom } from '../../models';
import { callbacks } from '../../callbacks';
import { hasPermission } from '../../authorization';
/*
* Join is a named function that will replace /topic commands
* @param {Object} message - The message object
*/
function Topic(command, params, item) {
if (command === 'topic') {

@ -201,8 +201,13 @@
text-overflow: ellipsis;
}
&-visual-status {
text-transform: capitalize;
&__visual-status {
overflow: hidden;
width: 100%;
max-width: fit-content;
text-overflow: ellipsis;
}
&__status {

@ -118,6 +118,30 @@
&--star-filled .rc-icon {
fill: currentColor;
}
&--online {
& .rc-popover__icon {
color: var(--status-online);
}
}
&--away {
& .rc-popover__icon {
color: var(--status-away);
}
}
&--busy {
& .rc-popover__icon {
color: var(--status-busy);
}
}
&--offline {
& .rc-popover__icon {
color: var(--status-invisible);
}
}
}
&__label {

@ -3742,58 +3742,58 @@ rc-old select,
}
}
& .edit-form {
padding: 20px 20px 0;
& .room-info-content > div {
margin: 0 0 20px;
}
}
white-space: normal;
.rc-old .edit-form {
padding: 20px 20px 0;
& h3 {
margin-bottom: 8px;
white-space: normal;
font-size: 24px;
line-height: 22px;
}
& h3 {
margin-bottom: 8px;
& p {
font-size: 12px;
font-weight: 300;
line-height: 18px;
}
font-size: 24px;
line-height: 22px;
}
& > .input-line {
margin-top: 20px;
& p {
font-size: 12px;
font-weight: 300;
line-height: 18px;
}
& #password {
width: 70%;
}
& > .input-line {
margin-top: 20px;
& #roleSelect {
width: 70%;
}
& #password {
width: 70%;
}
& nav {
padding: 0;
&.buttons {
margin-top: 2em;
}
& #roleSelect {
width: 70%;
}
}
& .form-divisor {
height: 9px;
margin: 2em 0;
text-align: center;
& nav {
padding: 0;
& > span {
padding: 0 1em;
}
&.buttons {
margin-top: 2em;
}
}
& .room-info-content > div {
margin: 0 0 20px;
& .form-divisor {
height: 9px;
margin: 2em 0;
text-align: center;
& > span {
padding: 0 1em;
}
}
}
@ -5370,9 +5370,8 @@ rc-old select,
position: absolute;
right: 25px;
width: 80px;
height: 30px;
padding-top: 4px;
padding: 4px;
cursor: pointer;
text-align: center;

@ -70,6 +70,26 @@
{{/if}}
</div>
</div>
<div class="rc-form-group rc-grid">
{{# with canChange=allowStatusMessageChange}}
<div class="rc-input rc-w100 padded {{#if statusMessageInvalid}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "StatusMessage"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="edit"}}
</div>
<input type="text" class="rc-input__element" maxlength="120" name="statusText" id="statusText" placeholder="{{_ "StatusMessage_Placeholder" }}" value="{{statusText}}" {{ifThenElse canChange '' 'disabled'}}>
</div>
</label>
{{# unless canChange}}
<div class="rc-input__description">{{_ 'StatusMessage_Change_Disabled'}}</div>
{{/unless}}
</div>
{{/with}}
</div>
<div class="rc-form-group rc-grid">
{{# with canChange=allowRealNameChange}}
<div class="rc-input rc-w50 padded {{#if nameInvalid}}rc-input--error{{/if}}">

@ -20,6 +20,11 @@ const validateUsername = (username) => {
return reg.test(username);
};
const validateName = (name) => name && name.length;
const validateStatusMessage = (statusMessage) => {
if (!statusMessage || statusMessage.length <= 120 || statusMessage.length === 0) {
return true;
}
};
const validatePassword = (password, confirmationPassword) => {
if (!confirmationPassword) {
return true;
@ -72,6 +77,9 @@ Template.accountProfile.helpers({
nameInvalid() {
return !validateName(Template.instance().realname.get());
},
statusMessageInvalid() {
return !validateStatusMessage(Template.instance().statusText.get());
},
confirmationPasswordInvalid() {
const { password, confirmationPassword } = Template.instance();
return !validatePassword(password.get(), confirmationPassword.get());
@ -109,6 +117,7 @@ Template.accountProfile.helpers({
const instance = Template.instance();
instance.dep.depend();
const realname = instance.realname.get();
const statusText = instance.statusText.get();
const username = instance.username.get();
const password = instance.password.get();
const confirmationPassword = instance.confirmationPassword.get();
@ -129,7 +138,7 @@ Template.accountProfile.helpers({
if (!avatar && user.name === realname && user.username === username && getUserEmailAddress(user) === email === email && (!password || password !== confirmationPassword)) {
return ret;
}
if (!validateEmail(email) || (!validateUsername(username) || usernameAvaliable !== true) || !validateName(realname)) {
if (!validateEmail(email) || (!validateUsername(username) || usernameAvaliable !== true) || !validateName(realname) || !validateStatusMessage(statusText)) {
return ret;
}
},
@ -142,6 +151,9 @@ Template.accountProfile.helpers({
username() {
return Meteor.user().username;
},
statusText() {
return Meteor.user().statusText;
},
email() {
const user = Meteor.user();
return getUserEmailAddress(user);
@ -153,6 +165,9 @@ Template.accountProfile.helpers({
allowRealNameChange() {
return settings.get('Accounts_AllowRealNameChange');
},
allowStatusMessageChange() {
return settings.get('Accounts_AllowUserStatusMessageChange');
},
allowUsernameChange() {
return settings.get('Accounts_AllowUsernameChange') && settings.get('LDAP_Enable') !== true;
},
@ -187,6 +202,7 @@ Template.accountProfile.onCreated(function() {
self.avatar = new ReactiveVar();
self.url = new ReactiveVar('');
self.usernameAvaliable = new ReactiveVar(true);
self.statusText = new ReactiveVar(user.statusText);
Notifications.onLogged('updateAvatar', () => self.avatar.set());
self.getSuggestions = function() {
@ -251,6 +267,16 @@ Template.accountProfile.onCreated(function() {
}
data.realname = s.trim(self.realname.get());
}
if (s.trim(self.statusText.get()) !== user.statusText) {
if (!settings.get('Accounts_AllowUserStatusMessageChange')) {
toastr.remove();
toastr.error(t('StatusMessage_Change_Disabled'));
instance.clearForm();
return cb && cb();
}
data.statusText = s.trim(self.statusText.get());
}
if (s.trim(self.username.get()) !== user.username) {
if (!settings.get('Accounts_AllowUsernameChange')) {
toastr.remove();
@ -370,6 +396,9 @@ Template.accountProfile.events({
'input [name=realname]'(e, instance) {
instance.realname.set(e.target.value);
},
'input [name=statusText]'(e, instance) {
instance.statusText.set(e.target.value);
},
'input [name=password]'(e, instance) {
instance.password.set(e.target.value);

@ -98,6 +98,20 @@
</div>
</div>
<div class="rc-form-group rc-form-group--small">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "StatusMessage"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon icon='edit' }}
</div>
<input name="name" type="text" maxlength="120" class="rc-input__element rc-input__element--small" id="status" autocomplete="off" value="{{user.statusText}}"/>
</div>
</label>
</div>
</div>
{{#if hasPermission 'edit-other-user-password'}}
<div class="rc-form-group rc-form-group--small rc-form-group--inline">

@ -182,6 +182,7 @@ Template.userEdit.onCreated(function() {
const userData = { _id: this.user != null ? this.user._id : undefined };
userData.name = s.trim(this.$('#name').val());
userData.username = s.trim(this.$('#username').val());
userData.statusText = s.trim(this.$('#status').val());
userData.email = s.trim(this.$('#email').val());
userData.verified = this.$('#verified:checked').length > 0;
userData.password = s.trim(this.$('#password').val());

@ -33,7 +33,7 @@
{{#if username}}<p class="rc-user-info__username">@{{username}}</p>{{/if}}
<span class="rc-header__status rc-user-info__status">
<div class="rc-header__status-bullet rc-header__status-bullet--{{userStatus}}" title="{{_ userStatus}}"></div>
<div class="rc-header__visual-status">{{_ userStatus}}</div>
<div class="rc-header__visual-status">{{userStatusText}}</div>
</span>
</div>

@ -8,11 +8,12 @@ import moment from 'moment';
import { DateFormat } from '../../../lib';
import { popover } from '../../../ui-utils';
import { templateVarHandler } from '../../../utils';
import { t, templateVarHandler } from '../../../utils';
import { RoomRoles, UserRoles, Roles } from '../../../models';
import { settings } from '../../../settings';
import FullUser from '../../../models/client/models/FullUser';
import { getActions } from './userActions';
import './userInfo.html';
const shownActionsCount = 2;
@ -85,6 +86,15 @@ Template.userInfo.helpers({
return userStatus || 'offline';
},
userStatusText() {
if (s.trim(this.statusText)) {
return this.statusText;
}
const user = Template.instance().user.get();
return t(Session.get(`user_${ user.username }_status`));
},
email() {
const user = Template.instance().user.get();
return user && user.emails && user.emails[0] && user.emails[0].address;

@ -86,14 +86,15 @@ Template.body.onRendered(function() {
return;
}
popover.close();
if (/input|textarea|select/i.test(target.tagName)) {
return;
}
if (target.id === 'pswp') {
return;
}
popover.close();
const inputMessage = chatMessages[RoomManager.openedRoom] && chatMessages[RoomManager.openedRoom].input;
if (!inputMessage) {
return;

@ -8,9 +8,10 @@ import { t, getUserPreference, handleError } from '../../utils';
import { callbacks } from '../../callbacks';
import { settings } from '../../settings';
import { hasAtLeastOnePermission } from '../../authorization';
import { userStatus } from '../../user-status';
const setStatus = (status) => {
AccountBox.setStatus(status);
const setStatus = (status, statusText) => {
AccountBox.setStatus(status, statusText);
callbacks.run('userStatusManuallySet', status);
popover.close();
};
@ -315,39 +316,61 @@ Template.sidebarHeader.events({
'click .sidebar__header .avatar'(e) {
if (!(Meteor.userId() == null && settings.get('Accounts_AllowAnonymousRead'))) {
const user = Meteor.user();
const userStatusList = Object.keys(userStatus.list).map((key) => {
const status = userStatus.list[key];
const customName = status.localizeName ? null : status.name;
const name = status.localizeName ? t(status.name) : status.name;
const modifier = status.statusType || user.status;
return {
icon: 'circle',
name,
modifier,
action: () => setStatus(status.statusType, customName),
};
});
const statusText = user.statusText || t(user.status);
userStatusList.push({
icon: 'edit',
name: t('Edit_Status'),
type: 'open',
action: (e) => {
e.preventDefault();
modal.open({
title: t('Edit_Status'),
content: 'editStatus',
data: {
onSave() {
modal.close();
},
},
modalClass: 'modal',
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,
});
},
});
const config = {
popoverClass: 'sidebar-header',
columns: [
{
groups: [
{
title: user.name,
items: [{
icon: 'circle',
name: statusText,
modifier: user.status,
}],
},
{
title: t('User'),
items: [
{
icon: 'circle',
name: t('online'),
modifier: 'online',
action: () => setStatus('online'),
},
{
icon: 'circle',
name: t('away'),
modifier: 'away',
action: () => setStatus('away'),
},
{
icon: 'circle',
name: t('busy'),
modifier: 'busy',
action: () => setStatus('busy'),
},
{
icon: 'circle',
name: t('invisible'),
modifier: 'offline',
action: () => setStatus('offline'),
},
],
items: userStatusList,
},
{
items: [

@ -11,8 +11,8 @@ import { SideNav } from './SideNav';
export const AccountBox = (function() {
let status = 0;
const items = new ReactiveVar([]);
function setStatus(status) {
return Meteor.call('UserPresence:setDefaultStatus', status);
function setStatus(status, statusText) {
return Meteor.call('setUserStatus', status, statusText);
}
function open() {
if (SideNav.flexStatus()) {

@ -19,7 +19,14 @@
{{> icon block="rc-popover__icon-element" icon=item.icon }}
</span>
{{/if}}
<span class="rc-popover__item-text">{{_ item.name}}</span>
{{#if item.name}}
<span class="rc-popover__item-text">{{item.name}}</span>
{{/if}}
{{#if item.select}}
<span class="rc-popover__input">
{{> selectDropdown title=item.selectTitle name=item.selectName options=item.selectOptions}}
</span>
{{/if}}
</li>
{{/with}}
{{/each}}

@ -42,7 +42,7 @@
{{#if isDirect}}
<span class="rc-header__status">
<div class="rc-header__status-bullet rc-header__status-bullet--{{userStatus}}" title="{{_ userStatus}}"></div>
<div class="rc-header__visual-status">{{_ userStatus}}</div>
<div class="rc-header__visual-status">{{userStatusText}}</div>
</span>
{{else}}
{{#if roomTopic}}<span class="rc-header__topic">{{{roomTopic}}}</span>{{/if}}

@ -4,6 +4,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
import s from 'underscore.string';
import { t, roomTypes, handleError } from '../../../../utils';
import { TabBar, fireGlobalEvent, call } from '../../../../ui-utils';
@ -22,6 +23,10 @@ const isDiscussion = ({ _id }) => {
return !!(room && room.prid);
};
const getUserStatus = (id) => {
const roomData = Session.get(`roomData${ id }`);
return roomTypes.getUserStatus(roomData.t, id) || 'offline';
};
Template.headerRoom.helpers({
back() {
@ -106,8 +111,18 @@ Template.headerRoom.helpers({
},
userStatus() {
return getUserStatus(this._id);
},
userStatusText() {
const roomData = Session.get(`roomData${ this._id }`);
return roomTypes.getUserStatus(roomData.t, this._id) || t('offline');
const statusText = roomTypes.getUserStatusText(roomData.t, this._id);
if (s.trim(statusText)) {
return statusText;
}
return t(getUserStatus(this._id));
},
showToggleFavorite() {

@ -1,19 +1,18 @@
<template name="selectDropdown">
<div class="rc-input">
<div class="rc-input rc-input--small">
<label class="rc-input__label">
<div class="rc-input__title">Invite people to channel</div>
<div class="rc-input__title">{{title}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="at"}}
</div>
{{ selectedUsers }}
<input type="text" class="rc-input__element" placeholder="Type user name" name="users">
<select type="text" class="rc-input__element" placeholder="{{placeholder}}" name="{{name}}">
{{#each option in options}}
{{#if option.selected}}
<option value={{option.value}} selected>{{option.title}}</option>
{{else}}
<option value={{option.value}}>{{option.title}}</option>
{{/if}}
{{/each}}
</select>
</div>
</label>
{{#if open}}
<div class="fadeInDown">
{{ > popupList .}}
</div>
{{/if}}
</div>
</template>

@ -1,23 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
Template.selectDropdown.events({
'focus input'(e, i) {
i.open.set(true);
console.log('asdasd');
},
'blur input'(e, i) {
setTimeout(() => {
i.open.set(false);
}, 100);
console.log('asdasd');
},
});
Template.selectDropdown.helpers({
open() {
return Template.instance().open.get();
},
});
Template.selectDropdown.onCreated(function() {
this.open = new ReactiveVar(false);
});

@ -15,6 +15,8 @@ import './views/404/invalidSecretURL.html';
import './views/app/audioNotification.html';
import './views/app/burger.html';
import './views/app/createChannel.html';
import './views/app/editStatus.html';
import './views/app/editStatus.css';
import './views/app/fullModal.html';
import './views/app/home.html';
import './views/app/notAuthorized.html';
@ -33,6 +35,7 @@ import './views/modal';
import './views/404/roomNotFound';
import './views/app/burger';
import './views/app/createChannel';
import './views/app/editStatus';
import './views/app/fullModal';
import './views/app/home';
import './views/app/directory';
@ -50,7 +53,7 @@ import './components/tabs';
import './components/popupList.html';
import './components/popupList';
import './components/selectDropdown.html';
import './components/selectDropdown';
import './components/header/header.html';
import './components/header/header';
import './components/header/headerRoom.html';

@ -16,7 +16,6 @@ const commands = {
FlowRouter.go(data.path, null, FlowRouter.current().queryParams);
},
'set-user-status'(data) {
AccountBox.setStatus(data.status);
},

@ -0,0 +1,45 @@
.edit-status-type.rc-popover {
&__item {
&--online {
color: var(--status-online);
}
&--away {
color: var(--status-away);
}
&--busy {
color: var(--status-busy);
}
&--offline {
color: var(--status-invisible);
}
}
}
.edit-status-type-icon {
&--online {
& .rc-icon {
color: var(--status-online);
}
}
&--away {
& .rc-icon {
color: var(--status-away);
}
}
&--busy {
& .rc-icon {
color: var(--status-busy);
}
}
&--offline {
& .rc-icon {
color: var(--status-invisible);
}
}
}

@ -0,0 +1,30 @@
<template name="editStatus">
<section class="edit-status">
<div class="edit-status__wrapper">
{{#if canChange}}
<form id="edit-status" name="edit-status" class="edit-status__content">
<div class="edit-status__inputs">
<div class="rc-input {{#if invalidChannel}}rc-input--error{{/if}}">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "StatusMessage"}}</div>
<div class="rc-input__wrapper edit-status">
<div class="rc-input__icon edit-status-type-icon--{{statusType}}">
{{> icon block="rc-input__icon-svg" icon="circle"}}
{{> icon block="rc-input__icon-svg" icon="arrow-down"}}
</div>
<input name="status" type="text" maxlength="120" class="rc-input__element" placeholder="{{_ "StatusMessage_Placeholder"}}" autofocus value="{{statusText}}">
<input type="hidden" name="statusType" value="{{statusType}}">
</div>
</label>
</div>
</div>
</form>
<div class="edit-status__inputs">
<input form='edit-status' class="rc-button rc-button--primary" type='submit' data-button="Save" value="{{_ "Save"}}" />
</div>
{{else}}
<div class="rc-input__description">{{_ "StatusMessage_Change_Disabled"}}</div>
{{/if}}
</div>
</section>
</template>

@ -0,0 +1,113 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import toastr from 'toastr';
import s from 'underscore.string';
import { settings } from '../../../../settings';
import { t } from '../../../../utils';
import { popover } from '../../../../ui-utils';
Template.editStatus.helpers({
canChange() {
return settings.get('Accounts_AllowUserStatusMessageChange');
},
statusType() {
return Meteor.user().status;
},
statusText() {
return Meteor.user().statusText;
},
});
Template.editStatus.events({
'click .edit-status .rc-input__icon'(e) {
const options = [
{
icon: 'circle',
name: t('Online'),
modifier: 'online',
action: () => {
$('input[name=statusType]').val('online');
$('.edit-status .rc-input__icon').prop('class', 'rc-input__icon edit-status-type-icon--online');
},
},
{
icon: 'circle',
name: t('Away'),
modifier: 'away',
action: () => {
$('input[name=statusType]').val('away');
$('.edit-status .rc-input__icon').prop('class', 'rc-input__icon edit-status-type-icon--away');
},
},
{
icon: 'circle',
name: t('Busy'),
modifier: 'busy',
action: () => {
$('input[name=statusType]').val('busy');
$('.edit-status .rc-input__icon').prop('class', 'rc-input__icon edit-status-type-icon--busy');
},
},
{
icon: 'circle',
name: t('Invisible'),
modifier: 'offline',
action: () => {
$('input[name=statusType]').val('offline');
$('.edit-status .rc-input__icon').prop('class', 'rc-input__icon edit-status-type-icon--offline');
},
},
];
const config = {
popoverClass: 'edit-status-type',
columns: [
{
groups: [
{
items: options,
},
],
},
],
currentTarget: e.currentTarget,
offsetVertical: e.currentTarget.clientHeight,
};
popover.open(config);
},
'submit .edit-status__content'(e, instance) {
e.preventDefault();
e.stopPropagation();
const statusText = s.trim(e.target.status.value);
const statusType = e.target.statusType.value;
if (statusText !== this.statusText) {
if (statusText.length > 120) {
toastr.remove();
toastr.error(t('StatusMessage_Too_Long'));
return false;
}
if (!settings.get('Accounts_AllowUserStatusMessageChange')) {
toastr.remove();
toastr.error(t('StatusMessage_Change_Disabled'));
return false;
}
if (statusText || statusText.length === 0) {
Meteor.call('setUserStatus', statusType, statusText);
if (instance.data.onSave) {
instance.data.onSave(true);
}
return;
}
}
return false;
},
});
Template.editStatus.onRendered(function() {
this.firstNode.querySelector('[name="status"]').focus();
});

@ -315,12 +315,13 @@ Template.room.helpers({
roomLeader() {
const roles = RoomRoles.findOne({ rid: this._id, roles: 'leader', 'u._id': { $ne: Meteor.userId() } });
if (roles) {
const leader = Users.findOne({ _id: roles.u._id }, { fields: { status: 1 } }) || {};
const leader = Users.findOne({ _id: roles.u._id }, { fields: { status: 1, statusText: 1 } }) || {};
return {
...roles.u,
name: settings.get('UI_Use_Real_Name') ? roles.u.name || roles.u.username : roles.u.username,
status: leader.status || 'offline',
statusDisplay: ((status) => status.charAt(0).toUpperCase() + status.slice(1))(leader.status || 'offline'),
statusDisplay: leader.statusText || leader.status || 'offline',
};
}
},
@ -386,11 +387,6 @@ Template.room.helpers({
return roomIcon;
},
userStatus() {
const { room } = Template.instance();
return roomTypes.getUserStatus(room.t, this._id) || 'offline';
},
maxMessageLength() {
return settings.get('Message_MaxAllowedSize');
},

@ -0,0 +1,72 @@
<template name="adminUserStatus">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Custom_User_Status"}}
<div class="content">
{{#unless hasPermission 'manage-user-status'}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<form class="search-form" role="form">
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{#if isReady}} {{> icon block="rc-input__icon-svg" icon="magnifier" }} {{else}} {{> loading }} {{/if}}
</div>
<input id="user-status-filter" type="text" class="rc-input__element" placeholder="{{_ " Search "}}" autofocus dir="auto">
</div>
</form>
<div class="results">
{{{_ "Showing_results" customUserStatus.length}}}
</div>
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr class="admin-table-row">
<th class="content-background-color border-component-color" width="1%">
<div class="table-fake-th">&nbsp;</div>
</th>
<th class="content-background-color border-component-color" width="49%">
<div class="table-fake-th">{{_ "Name"}}</div>
</th>
<th class="content-background-color border-component-color" width="50%">
<div class="table-fake-th">{{_ "Presence"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each customUserStatus}}
<tr class="user-status-info row-link admin-table-row">
<td class="border-component-color">
<div class="rc-table-wrapper userStatusAdminPreview">
<div class="rc-table-info">
<span class="rc-table-title">
{{> userStatusPreview name=name}}
</span>
</div>
</div>
</td>
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{name}}</span></div>
</div>
</td>
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{localizedStatusType}}</span></div>
</div>
</td>
</tr>
{{/each}}
</tbody>
{{/table}}
{{#if hasMore}}
<button class="button secondary load-more {{isLoading}}">{{_ "Load_more"}}</button>
{{/if}}
{{/unless}}
</div>
</section>
{{#with flexData}}
{{> flexTabBar}}
{{/with}}
</div>
</template>

@ -0,0 +1,137 @@
import s from 'underscore.string';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Tracker } from 'meteor/tracker';
import { CustomUserStatus } from '../../../models';
import { TabBar, SideNav, RocketChatTabBar } from '../../../ui-utils';
import { t } from '../../../utils';
Template.adminUserStatus.helpers({
isReady() {
if (Template.instance().ready != null) {
return Template.instance().ready.get();
}
return undefined;
},
customUserStatus() {
return Template.instance().customUserStatus().map((userStatus) => {
const { _id, name, statusType } = userStatus;
const localizedStatusType = statusType ? t(statusType) : '';
return {
_id,
name,
statusType,
localizedStatusType,
};
});
},
isLoading() {
if (Template.instance().ready != null) {
if (!Template.instance().ready.get()) {
return 'btn-loading';
}
}
},
hasMore() {
if (Template.instance().limit != null) {
if (typeof Template.instance().customUserStatus === 'function') {
return Template.instance().limit.get() === Template.instance().customUserStatus().length;
}
}
return false;
},
flexData() {
return {
tabBar: Template.instance().tabBar,
data: Template.instance().tabBarData.get(),
};
},
});
Template.adminUserStatus.onCreated(function() {
const instance = this;
this.limit = new ReactiveVar(50);
this.filter = new ReactiveVar('');
this.ready = new ReactiveVar(false);
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
this.tabBarData = new ReactiveVar();
TabBar.addButton({
groups: ['user-status-custom'],
id: 'add-user-status',
i18nTitle: 'Custom_User_Status_Add',
icon: 'plus',
template: 'adminUserStatusEdit',
order: 1,
});
TabBar.addButton({
groups: ['user-status-custom'],
id: 'admin-user-status-info',
i18nTitle: 'Custom_User_Status_Info',
icon: 'customize',
template: 'adminUserStatusInfo',
order: 2,
});
this.autorun(function() {
const limit = instance.limit !== null ? instance.limit.get() : 0;
const subscription = instance.subscribe('fullUserStatusData', '', limit);
instance.ready.set(subscription.ready());
});
this.customUserStatus = function() {
const filter = instance.filter != null ? s.trim(instance.filter.get()) : '';
let query = {};
if (filter) {
const filterReg = new RegExp(s.escapeRegExp(filter), 'i');
query = { $or: [{ name: filterReg }] };
}
const limit = instance.limit != null ? instance.limit.get() : 0;
return CustomUserStatus.find(query, { limit, sort: { name: 1 } }).fetch();
};
});
Template.adminUserStatus.onRendered(() =>
Tracker.afterFlush(function() {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
})
);
Template.adminUserStatus.events({
'keydown #user-status-filter'(e) {
// stop enter key
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
}
},
'keyup #user-status-filter'(e, t) {
e.stopPropagation();
e.preventDefault();
t.filter.set(e.currentTarget.value);
},
'click .user-status-info'(e, instance) {
e.preventDefault();
instance.tabBarData.set(CustomUserStatus.findOne({ _id: this._id }));
instance.tabBar.open('admin-user-status-info');
},
'click .load-more'(e, t) {
e.preventDefault();
e.stopPropagation();
t.limit.set(t.limit.get() + 50);
},
});

@ -0,0 +1,7 @@
<template name="adminUserStatusEdit">
<div class="content">
<div class="user-status-view">
{{> userStatusEdit .}}
</div>
</div>
</template>

@ -0,0 +1,7 @@
<template name="adminUserStatusInfo">
<div class="content">
<div class="user-status-view">
{{> userStatusInfo .}}
</div>
</div>
</template>

@ -0,0 +1,9 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
FlowRouter.route('/admin/user-status-custom', {
name: 'user-status-custom',
action(/* params */) {
BlazeLayout.render('main', { center: 'adminUserStatus' });
},
});

@ -0,0 +1,11 @@
import { AdminBox } from '../../../ui-utils';
import { hasAtLeastOnePermission } from '../../../authorization';
AdminBox.addOption({
href: 'user-status-custom',
i18nLabel: 'Custom_User_Status',
icon: 'user',
permissionGranted() {
return hasAtLeastOnePermission(['manage-user-status']);
},
});

@ -0,0 +1,42 @@
<template name="userStatusEdit">
{{#unless hasPermission 'manage-user-status'}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<div class="about">
<form class="edit-form" autocomplete="off">
{{#if userStatus}}
<h3>{{userStatus.name}}</h3>
{{else}}
<h3>{{_ "Custom_User_Status_Add"}}</h3>
{{/if}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Name"}}</div>
<div class="rc-input__wrapper">
<input name="name" type="text" value="{{userStatus.name}}" class="rc-input__element" placeholder="{{_ " Name "}}" autocomplete="off">
</div>
</label>
</div>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Presence"}}</div>
<div class="rc-select">
<select class="rc-select__element" name="statusType">
<option value="">{{_ "None"}}</option>
{{#each options}}
<option value="{{value}}" {{selected}}>{{name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
<nav class="rc-button__group rc-button__group--stretch">
<button class='rc-button rc-button--secondary cancel' type="button"><span>{{_ "Cancel"}}</span></button>
<button class='rc-button rc-button--primary save'><span>{{_ "Save"}}</span></button>
</nav>
</form>
</div>
{{/unless}}
</template>

@ -0,0 +1,115 @@
import toastr from 'toastr';
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
import { t, handleError } from '../../../utils';
Template.userStatusEdit.helpers({
userStatus() {
return Template.instance().userStatus;
},
options() {
const userStatusType = this.userStatus ? this.userStatus.statusType : '';
return [{
value: 'online',
name: t('Online'),
selected: userStatusType === 'online' ? 'selected' : '',
}, {
value: 'away',
name: t('Away'),
selected: userStatusType === 'away' ? 'selected' : '',
}, {
value: 'busy',
name: t('Busy'),
selected: userStatusType === 'busy' ? 'selected' : '',
}, {
value: 'offline',
name: t('Invisible'),
selected: userStatusType === 'offline' ? 'selected' : '',
}];
},
});
Template.userStatusEdit.events({
'click .cancel'(e, t) {
e.stopPropagation();
e.preventDefault();
t.cancel(t.find('form'));
},
'submit form'(e, t) {
e.stopPropagation();
e.preventDefault();
t.save(e.currentTarget);
},
});
Template.userStatusEdit.onCreated(function() {
if (this.data != null) {
this.userStatus = this.data.userStatus;
} else {
this.userStatus = undefined;
}
this.tabBar = Template.currentData().tabBar;
this.cancel = (form, name) => {
form.reset();
this.tabBar.close();
if (this.userStatus) {
this.data.back(name);
}
};
this.getUserStatusData = () => {
const userStatusData = {};
if (this.userStatus != null) {
userStatusData._id = this.userStatus._id;
userStatusData.previousName = this.userStatus.name;
}
userStatusData.name = s.trim(this.$('#name').val());
userStatusData.statusType = s.trim(this.$('#statusType').val());
return userStatusData;
};
this.validate = () => {
const userStatusData = this.getUserStatusData();
const errors = [];
if (!userStatusData.name) {
errors.push('Name');
}
for (const error of errors) {
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) }));
}
return errors.length === 0;
};
this.save = (form) => {
if (this.validate()) {
const userStatusData = this.getUserStatusData();
Meteor.call('insertOrUpdateUserStatus', userStatusData, (error, result) => {
if (result) {
if (userStatusData._id) {
toastr.success(t('Custom_User_Status_Updated_Successfully'));
} else {
toastr.success(t('Custom_User_Status_Added_Successfully'));
}
this.cancel(form, userStatusData.name);
}
if (error) {
handleError(error);
}
});
}
};
});

@ -0,0 +1,22 @@
<template name="userStatusInfo">
{{#if editingUserStatus}}
{{> userStatusEdit (userStatusToEdit)}}
{{else}}
{{#with userStatus}}
<div class="edit-form">
<div class="thumb">
{{> userStatusPreview name=name}}
</div>
<div class="info">
<h3 title="{{name}}">{{name}}</h3>
</div>
<nav class="rc-button__group rc-button__group--stretch">
{{#if hasPermission 'manage-user-status'}}
<button class='rc-button rc-button--cancel delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button>
<button class='rc-button rc-button--primary edit-user-satus'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button>
{{/if}}
</nav>
</div>
{{/with}}
{{/if}}
</template>

@ -0,0 +1,117 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { t, handleError } from '../../../utils';
import { modal } from '../../../ui-utils';
Template.userStatusInfo.helpers({
name() {
const userStatus = Template.instance().userStatus.get();
return userStatus.name;
},
userStatus() {
return Template.instance().userStatus.get();
},
editingUserStatus() {
return Template.instance().editingUserStatus.get();
},
userStatusToEdit() {
const instance = Template.instance();
return {
tabBar: this.tabBar,
userStatus: instance.userStatus.get(),
back(name) {
instance.editingUserStatus.set();
if (name != null) {
const userStatus = instance.userStatus.get();
if (userStatus != null && userStatus.name != null && userStatus.name !== name) {
return instance.loadedName.set(name);
}
}
},
};
},
});
Template.userStatusInfo.events({
'click .thumb'(e) {
$(e.currentTarget).toggleClass('bigger');
},
'click .delete'(e, instance) {
e.stopPropagation();
e.preventDefault();
const userStatus = instance.userStatus.get();
if (userStatus != null) {
const { _id } = userStatus;
modal.open({
title: t('Are_you_sure'),
text: t('Custom_User_Status_Delete_Warning'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, function() {
Meteor.call('deleteCustomUserStatus', _id, (error/* , result */) => {
if (error) {
return handleError(error);
}
modal.open({
title: t('Deleted'),
text: t('Custom_User_Status_Has_Been_Deleted'),
type: 'success',
timer: 2000,
showConfirmButton: false,
});
instance.tabBar.close();
});
});
}
},
'click .edit-user-satus'(e, instance) {
e.stopPropagation();
e.preventDefault();
instance.editingUserStatus.set(instance.userStatus.get()._id);
},
});
Template.userStatusInfo.onCreated(function() {
this.userStatus = new ReactiveVar();
this.editingUserStatus = new ReactiveVar();
this.loadedName = new ReactiveVar();
this.tabBar = Template.currentData().tabBar;
this.autorun(() => {
const data = Template.currentData();
if (data != null && data.clear != null) {
this.clear = data.clear;
}
});
this.autorun(() => {
const data = Template.currentData();
const userStatus = this.userStatus.get();
if (userStatus != null && userStatus.name != null) {
this.loadedName.set(userStatus.name);
} else if (data != null && data.name != null) {
this.loadedName.set(data.name);
}
});
this.autorun(() => {
const data = Template.currentData();
this.userStatus.set(data);
});
});

@ -0,0 +1,5 @@
<template name="userStatusPreview">
<div class="userStatusAdminPreview">
<div class="userStatusAdminPreview" data-user-status="{{this.name}}"></div>
</div>
</template>

@ -0,0 +1,17 @@
import './admin/adminUserStatus.html';
import './admin/adminUserStatus';
import './admin/adminUserStatusEdit.html';
import './admin/adminUserStatusInfo.html';
import './admin/userStatusEdit.html';
import './admin/userStatusEdit';
import './admin/userStatusInfo.html';
import './admin/userStatusInfo';
import './admin/userStatusPreview.html';
import './admin/route';
import './admin/startup';
import './notifications/deleteCustomUserStatus';
import './notifications/updateCustomUserStatus';
export { userStatus } from './lib/userStatus';
export { deleteCustomUserStatus, updateCustomUserStatus } from './lib/customUserStatus';

@ -0,0 +1,54 @@
import { Meteor } from 'meteor/meteor';
import { userStatus } from './userStatus';
userStatus.packages.customUserStatus = {
list: [],
};
export const deleteCustomUserStatus = function(customUserStatusData) {
delete userStatus.list[customUserStatusData._id];
const arrayIndex = userStatus.packages.customUserStatus.list.indexOf(customUserStatusData._id);
if (arrayIndex !== -1) {
userStatus.packages.customUserStatusData.list.splice(arrayIndex, 1);
}
};
export const updateCustomUserStatus = function(customUserStatusData) {
const newUserStatus = {
name: customUserStatusData.name,
id: customUserStatusData._id,
statusType: customUserStatusData.statusType,
localizeName: false,
};
const arrayIndex = userStatus.packages.customUserStatus.list.indexOf(newUserStatus.id);
if (arrayIndex === -1) {
userStatus.packages.customUserStatus.list.push(newUserStatus);
} else {
userStatus.packages.customUserStatus.list[arrayIndex] = newUserStatus;
}
userStatus.list[newUserStatus.id] = newUserStatus;
};
Meteor.startup(() =>
Meteor.call('listCustomUserStatus', (error, result) => {
if (!result) {
return;
}
for (const customStatus of result) {
const newUserStatus = {
name: customStatus.name,
id: customStatus._id,
statusType: customStatus.statusType,
localizeName: false,
};
userStatus.packages.customUserStatus.list.push(newUserStatus);
userStatus.list[newUserStatus.id] = newUserStatus;
}
})
);

@ -0,0 +1,36 @@
export const userStatus = {
packages: {
base: {
render(html) {
return html;
},
},
},
list: {
online: {
name: 'online',
localizeName: true,
id: 'online',
statusType: 'online',
},
away: {
name: 'away',
localizeName: true,
id: 'away',
statusType: 'away',
},
busy: {
name: 'busy',
localizeName: true,
id: 'busy',
statusType: 'busy',
},
invisible: {
name: 'invisible',
localizeName: true,
id: 'offline',
statusType: 'offline',
},
},
};

@ -0,0 +1,8 @@
import { Meteor } from 'meteor/meteor';
import { deleteCustomUserStatus } from '../lib/customUserStatus';
import { Notifications } from '../../../notifications';
Meteor.startup(() =>
Notifications.onLogged('deleteCustomUserStatus', (data) => deleteCustomUserStatus(data.userStatusData))
);

@ -0,0 +1,8 @@
import { Meteor } from 'meteor/meteor';
import { updateCustomUserStatus } from '../lib/customUserStatus';
import { Notifications } from '../../../notifications';
Meteor.startup(() =>
Notifications.onLogged('updateCustomUserStatus', (data) => updateCustomUserStatus(data.userStatusData))
);

@ -0,0 +1,8 @@
import { Meteor } from 'meteor/meteor';
if (Meteor.isClient) {
module.exports = require('./client/index.js');
}
if (Meteor.isServer) {
module.exports = require('./server/index.js');
}

@ -0,0 +1,6 @@
import './methods/deleteCustomUserStatus';
import './methods/insertOrUpdateUserStatus';
import './methods/listCustomUserStatus';
import './methods/setUserStatus';
import './publications/fullUserStatusData';

@ -0,0 +1,26 @@
import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
import { Notifications } from '../../../notifications';
import { CustomUserStatus } from '../../../models';
Meteor.methods({
deleteCustomUserStatus(userStatusID) {
let userStatus = null;
if (hasPermission(this.userId, 'manage-user-status')) {
userStatus = CustomUserStatus.findOneById(userStatusID);
} else {
throw new Meteor.Error('not_authorized');
}
if (userStatus == null) {
throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' });
}
CustomUserStatus.removeById(userStatusID);
Notifications.notifyLogged('deleteCustomUserStatus', { userStatusData: userStatus });
return true;
},
});

@ -0,0 +1,70 @@
import { Meteor } from 'meteor/meteor';
import s from 'underscore.string';
import { hasPermission } from '../../../authorization';
import { Notifications } from '../../../notifications';
import { CustomUserStatus } from '../../../models';
Meteor.methods({
insertOrUpdateUserStatus(userStatusData) {
if (!hasPermission(this.userId, 'manage-user-status')) {
throw new Meteor.Error('not_authorized');
}
if (!s.trim(userStatusData.name)) {
throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { method: 'insertOrUpdateUserStatus', field: 'Name' });
}
// allow all characters except >, <, &, ", '
// more practical than allowing specific sets of characters; also allows foreign languages
const nameValidation = /[><&"']/;
if (nameValidation.test(userStatusData.name)) {
throw new Meteor.Error('error-input-is-not-a-valid-field', `${ userStatusData.name } is not a valid name`, { method: 'insertOrUpdateUserStatus', input: userStatusData.name, field: 'Name' });
}
let matchingResults = [];
if (userStatusData._id) {
matchingResults = CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).fetch();
} else {
matchingResults = CustomUserStatus.findByName(userStatusData.name).fetch();
}
if (matchingResults.length > 0) {
throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', { method: 'insertOrUpdateUserStatus' });
}
const validStatusTypes = ['online', 'away', 'busy', 'offline'];
if (userStatusData.statusType && validStatusTypes.indexOf(userStatusData.statusType) < 0) {
throw new Meteor.Error('error-input-is-not-a-valid-field', `${ userStatusData.statusType } is not a valid status type`, { method: 'insertOrUpdateUserStatus', input: userStatusData.statusType, field: 'StatusType' });
}
if (!userStatusData._id) {
// insert user status
const createUserStatus = {
name: userStatusData.name,
statusType: userStatusData.statusType || null,
};
const _id = CustomUserStatus.create(createUserStatus);
Notifications.notifyLogged('updateCustomUserStatus', { userStatusData: createUserStatus });
return _id;
}
// update User status
if (userStatusData.name !== userStatusData.previousName) {
CustomUserStatus.setName(userStatusData._id, userStatusData.name);
}
if (userStatusData.statusType !== userStatusData.previousStatusType) {
CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType);
}
Notifications.notifyLogged('updateCustomUserStatus', { userStatusData });
return true;
},
});

@ -0,0 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { CustomUserStatus } from '../../../models';
Meteor.methods({
listCustomUserStatus() {
return CustomUserStatus.find({}).fetch();
},
});

@ -0,0 +1,30 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { settings } from '../../../settings';
import { RateLimiter, setStatusMessage } from '../../../lib';
Meteor.methods({
setUserStatus(statusType, statusText) {
if (statusType) {
Meteor.call('UserPresence:setDefaultStatus', statusType);
}
if (statusText || statusText === '') {
check(statusText, String);
if (!settings.get('Accounts_AllowUserStatusMessageChange')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'setUserStatus',
});
}
const userId = Meteor.userId();
setStatusMessage(userId, statusText);
}
},
});
RateLimiter.limitMethod('setUserStatus', 1, 1000, {
userId: () => true,
});

@ -0,0 +1,30 @@
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
import { CustomUserStatus } from '../../../models';
Meteor.publish('fullUserStatusData', function(filter, limit) {
if (!this.userId) {
return this.ready();
}
const fields = {
name: 1,
statusType: 1,
};
filter = s.trim(filter);
const options = {
fields,
limit,
sort: { name: 1 },
};
if (filter) {
const filterReg = new RegExp(s.escapeRegExp(filter), 'i');
return CustomUserStatus.findByName(filterReg, options);
}
return CustomUserStatus.find({}, options);
});

@ -51,6 +51,10 @@ export const roomTypes = new class RocketChatRoomTypes extends RoomTypesCommon {
return room && room.t;
}
getUserStatusText(roomType, rid) {
return this.roomTypes[roomType] && typeof this.roomTypes[roomType].getUserStatusText === 'function' && this.roomTypes[roomType].getUserStatusText(rid);
}
findRoom(roomType, identifier, user) {
return this.roomTypes[roomType] && this.roomTypes[roomType].findRoom(identifier, user);
}

@ -4,6 +4,7 @@ export const getDefaultUserFields = () => ({
emails: 1,
status: 1,
statusDefault: 1,
statusText: 1,
statusConnection: 1,
avatarOrigin: 1,
utcOffset: 1,

@ -97,6 +97,7 @@ import '../app/lazy-load';
import '../app/discussion/client';
import '../app/threads/client';
import '../app/mail-messages/client';
import '../app/user-status';
import '../app/utils';
import '../app/settings';
import '../app/models';

@ -4,13 +4,15 @@ import { Session } from 'meteor/session';
import { RoomManager } from '../../app/ui-utils';
Meteor.startup(function() {
Meteor.users.find({}, { fields: { name: 1, username: 1, status: 1, utcOffset: 1 } }).observe({
Meteor.users.find({}, { fields: { name: 1, username: 1, status: 1, utcOffset: 1, statusText: 1 } }).observe({
added(user) {
Session.set(`user_${ user.username }_status`, user.status);
Session.set(`user_${ user.username }_status_text`, user.statusText);
RoomManager.updateUserStatus(user, user.status, user.utcOffset);
},
changed(user) {
Session.set(`user_${ user.username }_status`, user.status);
Session.set(`user_${ user.username }_status_text`, user.statusText);
RoomManager.updateUserStatus(user, user.status, user.utcOffset);
},
removed(user) {

@ -25,6 +25,7 @@ const saveUser = (user, force = false) => {
// name: user.name,
// utcOffset: user.utcOffset,
status: user.status,
statusText: user.statusText,
},
});
}

@ -38,6 +38,7 @@
"Accounts_AllowUserAvatarChange": "Allow User Avatar Change",
"Accounts_AllowUsernameChange": "Allow Username Change",
"Accounts_AllowUserProfileChange": "Allow User Profile Change",
"Accounts_AllowUserStatusMessageChange": "Allow Custom Status Message",
"Accounts_AvatarBlockUnauthenticatedAccess": "Block Unauthenticated Access to Avatars",
"Accounts_AvatarCacheTime": "Avatar cache time",
"Accounts_AvatarCacheTime_description": "Number of seconds the http protocol is told to cache the avatar images.",
@ -973,6 +974,15 @@
"Custom_Sounds": "Custom Sounds",
"Custom_Translations": "Custom Translations",
"Custom_Translations_Description": "Should be a valid JSON where keys are languages containing a dictionary of key and translations. Example:<br/><code>{\n\"en\": {\n\"Channels\": \"Rooms\"\n},\n\"pt\": {\n\"Channels\": \"Salas\"\n}\n}</code> ",
"Custom_User_Status": "Custom User Status",
"Custom_User_Status_Add": "Add Custom User Status",
"Custom_User_Status_Added_Successfully" : "Custom User Status Added Successfully",
"Custom_User_Status_Delete_Warning": "Deleting a Custom User Status cannot be undone.",
"Custom_User_Status_Error_Invalid_User_Status": "Invalid User Status",
"Custom_User_Status_Error_Name_Already_In_Use": "The Custom User Status Name is already in use.",
"Custom_User_Status_Has_Been_Deleted": "Custom User Status Has Been Deleted",
"Custom_User_Status_Info": "Custom User Status Info",
"Custom_User_Status_Updated_Successfully": "Custom User Status Updated Successfully",
"Customize": "Customize",
"CustomSoundsFilesystem": "Custom Sounds Filesystem",
"Dashboard": "Dashboard",
@ -1112,6 +1122,7 @@
"E2E_password_reveal_text": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store this password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on. <a href=\"https://rocket.chat/docs/user-guides/end-to-end-encryption/\" target=\"_blank\">Learn more here!</a><br/><br/>Your password is: <span style=\"font-weight: bold;\">%s</span><br/><br/>This is an auto generated password, you can setup a new password for your encryption key any time from any browser you have entered the existing password.<br/>This password is only stored on this browser until you store the password and dismiss this message.",
"E2E_password_request_text": "To access your encrypted private groups and direct messages, enter your encryption password. <br/>You need to enter this password to encode/decode your messages on every client you use, since the key is not stored on the server.",
"Edit": "Edit",
"Edit_Status": "Edit Status",
"edit-message": "Edit Message",
"edit-message_description": "Permission to edit a message within a room",
"edit-other-user-active-status": "Edit Other User Active Status",
@ -1941,6 +1952,8 @@
"manage-own-integrations_description": "Permition to allow users to create and edit their own integration or webhooks",
"manage-sounds": "Manage Sounds",
"manage-sounds_description": "Permission to manage the server sounds",
"manage-user-status": "Manage User Status",
"manage-user-status_description": "Permission to manage the server custom user statuses",
"Manage_Apps": "Manage Apps",
"Manage_the_App": "Manage the App",
"Manager_added": "Manager added",
@ -2320,6 +2333,7 @@
"Preparing_list_of_channels": "Preparing list of channels",
"Preparing_list_of_messages": "Preparing list of messages",
"Preparing_list_of_users": "Preparing list of users",
"Presence": "Presence",
"preview-c-room": "Preview Public Channel",
"preview-c-room_description": "Permission to view the contents of a public channel before joining",
"Previous_month": "Previous Month",
@ -2698,6 +2712,8 @@
"Slash_Shrug_Description": "Displays ¯\\_(ツ)_/¯ after your message",
"Slash_Tableflip_Description": "Displays (╯°□°)╯︵ ┻━┻",
"Slash_TableUnflip_Description": "Displays ┬─┬ ノ( ゜-゜ノ)",
"Slash_Status_Description": "Set your status message",
"Slash_Status_Params": "Status message",
"Slash_Topic_Description": "Set topic",
"Slash_Topic_Params": "Topic message",
"Smarsh_Email": "Smarsh Email",
@ -2772,6 +2788,11 @@
"Stats_Total_Uploads": "Total Uploads",
"Stats_Total_Uploads_Size": "Total Uploads Size",
"Status": "Status",
"StatusMessage": "Status Message",
"StatusMessage_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of status messages",
"StatusMessage_Changed_Successfully": "Status message changed successfully.",
"StatusMessage_Placeholder": "What are you doing right now?",
"StatusMessage_Too_Long": "Status message must be shorter than 120 characters.",
"Step": "Step",
"Stop_Recording": "Stop Recording",
"Store_Last_Message": "Store Last Message",

@ -38,6 +38,7 @@
"Accounts_AllowUserAvatarChange": "Permitir al Usuario modificar su Avatar",
"Accounts_AllowUsernameChange": "Permitir el Cambio de Nombre de Usuario",
"Accounts_AllowUserProfileChange": "Permitir al Usuario modificar su Perfil",
"Accounts_AllowUserStatusMessageChange": "Permitir cambio de mensaje de estado",
"Accounts_AvatarCacheTime": "Tiempo de caché de Avatar",
"Accounts_AvatarCacheTime_description": "Número de segundos que se le dice al protocolo http para almacenar en caché las imágenes de avatar.",
"Accounts_AvatarResize": "Cambiar el Tamaño de los Avatars",
@ -2377,6 +2378,8 @@
"Slash_Shrug_Description": "Muestra ¯ \\ _ (ツ) _ / ¯ después de su mensaje",
"Slash_Tableflip_Description": "Muestra ° (╯ ° □ °) ╯( ┻━┻",
"Slash_TableUnflip_Description": "Muestra ┬─┬ ノ (゜ - ゜ ノ)",
"Slash_Status_Description": "Configura tu mensaje de estado",
"Slash_Status_Params": "Mensaje de estado",
"Slash_Topic_Description": "Establecer tema",
"Slash_Topic_Params": "Mensaje del tema",
"Smarsh_Email": "Smarsh Email",
@ -2444,6 +2447,11 @@
"Stats_Total_Rooms": "Total de Salas",
"Stats_Total_Users": "Total de Usuarios",
"Status": "Estado",
"StatusMessage": "Mensaje de estado",
"StatusMessage_Change_Disabled": "Tu administrador de Rocket.Chat ha desactivado el cambio de mensajes de estado",
"StatusMessage_Changed_Successfully": "Mensaje de estado cambiado correctamente.",
"StatusMessage_Placeholder": "¿Qué estás haciendo ahora?",
"StatusMessage_Too_Long": "El mensaje de estado debe tener menos de 120 caracteres.",
"Step": "Paso",
"Stop_Recording": "Detener Grabacion",
"Store_Last_Message": "Almacenar el último mensaje",

@ -2280,6 +2280,7 @@
"Preparing_list_of_channels": "Preparando lista de canais",
"Preparing_list_of_messages": "Preparando lista de mensagens",
"Preparing_list_of_users": "Preparando lista de usuários",
"Presence": "Presença",
"preview-c-room": "Pré-visualizar Canal público",
"preview-c-room_description": "Permissão para visualizar o conteúdo de um canal público antes de se juntar",
"Previous_month": "Mês anterior",

@ -81,6 +81,7 @@ import '../app/slashcommands-leave';
import '../app/slashcommands-me';
import '../app/slashcommands-msg';
import '../app/slashcommands-mute';
import '../app/slashcommands-status';
import '../app/slashcommands-topic/server';
import '../app/slashcommands-unarchiveroom/server';
import '../app/smarsh-connector';
@ -105,6 +106,7 @@ import '../app/chatpal-search/server';
import '../app/discussion/server';
import '../app/bigbluebutton';
import '../app/mail-messages/server';
import '../app/user-status';
import '../app/utils';
import '../app/settings';
import '../app/models';

@ -49,6 +49,10 @@ Meteor.methods({
Meteor.call('setUsername', settings.username);
}
if (settings.statusText || settings.statusText === '') {
Meteor.call('setUserStatus', null, settings.statusText);
}
if (settings.email) {
if (!checkPassword(user, settings.typedPassword)) {
throw new Meteor.Error('error-invalid-password', 'Invalid password', {

@ -13,6 +13,7 @@ Meteor.publish('activeUsers', function() {
name: 1,
status: 1,
utcOffset: 1,
statusText: 1,
},
});
});

@ -495,6 +495,7 @@ describe('[Users]', function() {
updateSetting('Accounts_AllowUserProfileChange', true)
.then(() => updateSetting('Accounts_AllowUsernameChange', true))
.then(() => updateSetting('Accounts_AllowRealNameChange', true))
.then(() => updateSetting('Accounts_AllowUserStatusMessageChange', true))
.then(() => updateSetting('Accounts_AllowEmailChange', true))
.then(() => updateSetting('Accounts_AllowPasswordChange', true))
.then(done);
@ -503,6 +504,7 @@ describe('[Users]', function() {
updateSetting('Accounts_AllowUserProfileChange', true)
.then(() => updateSetting('Accounts_AllowUsernameChange', true))
.then(() => updateSetting('Accounts_AllowRealNameChange', true))
.then(() => updateSetting('Accounts_AllowUserStatusMessageChange', true))
.then(() => updateSetting('Accounts_AllowEmailChange', true))
.then(() => updateSetting('Accounts_AllowPasswordChange', true))
.then(done);
@ -662,6 +664,50 @@ describe('[Users]', function() {
});
});
it('should return an error when trying update user status message and it is not allowed', (done) => {
updatePermission('edit-other-user-info', ['user']).then(() => {
updateSetting('Accounts_AllowUserStatusMessageChange', false)
.then(() => {
request.post(api('users.update'))
.set(credentials)
.send({
userId: targetUser._id,
data: {
statusMessage: 'a new status',
},
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
});
});
it('should update user status message when the required permission is applied', (done) => {
updatePermission('edit-other-user-info', ['admin']).then(() => {
updateSetting('Accounts_AllowUserStatusMessageChange', false)
.then(() => {
request.post(api('users.update'))
.set(credentials)
.send({
userId: targetUser._id,
data: {
name: 'a new status',
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
});
it('should return an error when trying update user email and it is not allowed', (done) => {
updatePermission('edit-other-user-info', ['user']).then(() => {
updateSetting('Accounts_AllowEmailChange', false)

@ -364,6 +364,10 @@ class Administration extends Page {
get accountsRealNameChangeFalse() { return browser.element('label:nth-of-type(2) [name="Accounts_AllowRealNameChange"]'); }
get accountsUserStatusMessageChangeTrue() { return browser.element('label:nth-of-type(1) [name="Accounts_AllowUserStatusMessageChange"]'); }
get accountsUserStatusMessageChangeFalse() { return browser.element('label:nth-of-type(2) [name="Accounts_AllowUserStatusMessageChange"]'); }
get accountsUsernameChangeTrue() { return browser.element('label:nth-of-type(1) [name="Accounts_AllowUsernameChange"]'); }
get accountsUsernameChangeFalse() { return browser.element('label:nth-of-type(2) [name="Accounts_AllowUsernameChange"]'); }

Loading…
Cancel
Save