[FIX] Custom status fixes (#14853)

* Fixes for status message text presence issues
Added statusText to several api endpoints
Changed statusMessage to statusText since that is what it is called everywhere

* Fixed slash command for changing status

* Fixed the "name is required" issue

* Make sure the status is set blank if selecting a default status... we don't want an "online" status when someone is actually offline!

* Fixes display of custom status on the room header

* Changed the header of DM rooms to query the server for the user status text when it is not found on the internal collection

* Changed Custom Status methods to check if the user is logged in

* Improved code readability

* Fix getting status list before logging in
pull/14886/head
William Reiske 6 years ago committed by Diego Sampaio
parent 0e95941d02
commit 24ebd2c434
  1. 2
      app/api/server/v1/channels.js
  2. 2
      app/api/server/v1/groups.js
  3. 2
      app/api/server/v1/im.js
  4. 4
      app/api/server/v1/users.js
  5. 11
      app/lib/lib/roomTypes/direct.js
  6. 19
      app/lib/server/functions/getStatusText.js
  7. 3
      app/lib/server/functions/index.js
  8. 4
      app/lib/server/functions/saveUser.js
  9. 45
      app/lib/server/functions/setStatusMessage.js
  10. 53
      app/lib/server/functions/setStatusText.js
  11. 41
      app/slashcommands-status/lib/status.js
  12. 7
      app/ui-flextab/client/tabs/userInfo.js
  13. 14
      app/ui-sidenav/client/sidebarHeader.js
  14. 59
      app/ui/client/components/header/headerRoom.js
  15. 4
      app/user-status/client/admin/userStatusEdit.html
  16. 39
      app/user-status/client/lib/customUserStatus.js
  17. 1
      app/user-status/server/index.js
  18. 14
      app/user-status/server/methods/getUserStatusText.js
  19. 5
      app/user-status/server/methods/listCustomUserStatus.js
  20. 10
      app/user-status/server/methods/setUserStatus.js
  21. 5
      client/startup/usersObserve.js
  22. 4
      imports/startup/client/listenActiveUsers.js
  23. 2
      imports/users-presence/server/activeUsers.js
  24. 1
      server/publications/spotlight.js

@ -539,7 +539,7 @@ API.v1.addRoute('channels.members', { authRequired: true }, {
const members = subscriptions.fetch().map((s) => s.u && s.u._id);
const users = Users.find({ _id: { $in: members } }, {
fields: { _id: 1, username: 1, name: 1, status: 1, utcOffset: 1 },
fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 },
sort: { username: sort.username != null ? sort.username : 1 },
}).fetch();

@ -497,7 +497,7 @@ API.v1.addRoute('groups.members', { authRequired: true }, {
const members = subscriptions.fetch().map((s) => s.u && s.u._id);
const users = Users.find({ _id: { $in: members } }, {
fields: { _id: 1, username: 1, name: 1, status: 1, utcOffset: 1 },
fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 },
sort: { username: sort.username != null ? sort.username : 1 },
}).fetch();

@ -205,7 +205,7 @@ API.v1.addRoute(['dm.members', 'im.members'], { authRequired: true }, {
const members = cursor.fetch().map((s) => s.u && s.u.username);
const users = Users.find({ username: { $in: members } }, {
fields: { _id: 1, username: 1, name: 1, status: 1, utcOffset: 1 },
fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 },
sort: { username: sort && sort.username ? sort.username : 1 },
}).fetch();

@ -18,7 +18,7 @@ import {
} from '../../../lib';
import { getFullUserData } from '../../../lib/server/functions/getFullUserData';
import { API } from '../api';
import { setStatusMessage } from '../../../lib/server';
import { setStatusText } from '../../../lib/server';
API.v1.addRoute('users.create', { authRequired: true }, {
post() {
@ -370,7 +370,7 @@ API.v1.addRoute('users.setStatus', { authRequired: true }, {
Meteor.runAsUser(user._id, () => {
if (this.bodyParams.message) {
setStatusMessage(user._id, this.bodyParams.message);
setStatusText(user._id, this.bodyParams.message);
}
if (this.bodyParams.status) {
const validStatus = ['online', 'away', 'offline', 'busy'];

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

@ -0,0 +1,19 @@
import { Users } from '../../../models/server';
export const getStatusText = function(userId) {
if (!userId) {
return undefined;
}
const fields = {
statusText: 1,
};
const options = {
fields,
limit: 1,
};
const data = Users.findOneById(userId, options);
return data && data.statusText;
};

@ -23,7 +23,8 @@ export { saveUser } from './saveUser';
export { sendMessage } from './sendMessage';
export { setEmail } from './setEmail';
export { setRealName, _setRealName } from './setRealName';
export { setStatusMessage, _setStatusMessage } from './setStatusMessage';
export { setStatusText, _setStatusText } from './setStatusText';
export { getStatusText } from './getStatusText';
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, setStatusMessage } from '.';
import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setRealName, setUsername, setStatusText } from '.';
const passwordPolicy = new PasswordPolicy();
@ -256,7 +256,7 @@ export const saveUser = function(userId, userData) {
}
if (typeof userData.statusText === 'string') {
setStatusMessage(userData._id, userData.statusText);
setStatusText(userData._id, userData.statusText);
}
if (userData.email) {

@ -1,45 +0,0 @@
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');
},
});

@ -0,0 +1,53 @@
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';
// mirror of object in /imports/startup/client/listenActiveUsers.js - keep updated
const STATUS_MAP = {
offline: 0,
online: 1,
away: 2,
busy: 3,
};
export const _setStatusText = function(userId, statusText) {
statusText = s.trim(statusText);
if (statusText.length > 120) {
statusText = statusText.substr(0, 120);
}
if (!userId) {
return false;
}
const user = Users.findOneById(userId);
// User already has desired statusText, return
if (user.statusText === statusText) {
return user;
}
// Set new statusText
Users.updateStatusText(user._id, statusText);
user.statusText = statusText;
Notifications.notifyLogged('user-status', [
user._id,
user.username,
STATUS_MAP[user.status],
statusText,
]);
return true;
};
export const setStatusText = RateLimiter.limitFunction(_setStatusText, 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');
},
});

@ -3,40 +3,37 @@ 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());
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),
});
}
Meteor.call('setUserStatus', null, params, (err) => {
if (err) {
if (Meteor.isClient) {
return handleError(err);
}
throw err;
} else {
if (err.error === 'error-not-allowed') {
Notifications.notifyUser(Meteor.userId(), 'message', {
_id: Random.id(),
rid: item.rid,
ts: new Date(),
msg: TAPi18n.__('StatusMessage_Changed_Successfully', null, user.language),
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),
});
}
});
}
}

@ -8,7 +8,7 @@ import moment from 'moment';
import { DateFormat } from '../../../lib';
import { popover } from '../../../ui-utils';
import { t, templateVarHandler } from '../../../utils';
import { templateVarHandler } from '../../../utils';
import { RoomRoles, UserRoles, Roles } from '../../../models';
import { settings } from '../../../settings';
import FullUser from '../../../models/client/models/FullUser';
@ -83,7 +83,7 @@ Template.userInfo.helpers({
userStatus() {
const user = Template.instance().user.get();
const userStatus = Session.get(`user_${ user.username }_status`);
return userStatus || 'offline';
return userStatus || TAPi18n.__('offline');
},
userStatusText() {
@ -92,7 +92,8 @@ Template.userInfo.helpers({
}
const user = Template.instance().user.get();
return t(Session.get(`user_${ user.username }_status`));
const userStatus = Session.get(`user_${ user.username }_status`);
return userStatus || TAPi18n.__('offline');
},
email() {

@ -295,7 +295,7 @@ Template.sidebarHeader.helpers({
};
}
return id && Meteor.users.findOne(id, { fields: {
username: 1, status: 1,
username: 1, status: 1, statusText: 1,
} });
},
toolbarButtons() {
@ -316,18 +316,24 @@ Template.sidebarHeader.events({
'click .sidebar__header .avatar'(e) {
if (!(Meteor.userId() == null && settings.get('Accounts_AllowAnonymousRead'))) {
const user = Meteor.user();
const STATUS_MAP = [
'offline',
'online',
'away',
'busy',
];
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;
const defaultStatus = STATUS_MAP.includes(status.id);
const statusText = defaultStatus ? null : name;
return {
icon: 'circle',
name,
modifier,
action: () => setStatus(status.statusType, customName),
action: () => setStatus(status.statusType, statusText),
};
});

@ -4,7 +4,6 @@ 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';
@ -25,7 +24,12 @@ const isDiscussion = ({ _id }) => {
const getUserStatus = (id) => {
const roomData = Session.get(`roomData${ id }`);
return roomTypes.getUserStatus(roomData.t, id) || 'offline';
return roomTypes.getUserStatus(roomData.t, id);
};
const getUserStatusText = (id) => {
const roomData = Session.get(`roomData${ id }`);
return roomTypes.getUserStatusText(roomData.t, id);
};
Template.headerRoom.helpers({
@ -111,18 +115,26 @@ Template.headerRoom.helpers({
},
userStatus() {
return getUserStatus(this._id);
return getUserStatus(this._id) || 'offline';
},
userStatusText() {
const roomData = Session.get(`roomData${ this._id }`);
const statusText = roomTypes.getUserStatusText(roomData.t, this._id);
if (s.trim(statusText)) {
const statusText = getUserStatusText(this._id);
if (statusText) {
return statusText;
}
return t(getUserStatus(this._id));
const presence = getUserStatus(this._id);
if (presence) {
return t(presence);
}
const oldStatusText = Template.instance().userOldStatusText.get();
if (oldStatusText) {
return oldStatusText;
}
return t('offline');
},
showToggleFavorite() {
@ -183,10 +195,39 @@ Template.headerRoom.events({
},
});
const loadUserStatusText = () => {
const instance = Template.instance();
if (!instance || !instance.data || !instance.data._id) {
return;
}
const id = instance.data._id;
if (Rooms.findOne(id).t !== 'd') {
return;
}
const userId = id.replace(Meteor.userId(), '');
// If the user is already on the local collection, the method call is not necessary
const found = Meteor.users.findOne(userId, { fields: { _id: 1 } });
if (found) {
return;
}
Meteor.call('getUserStatusText', userId, (error, result) => {
if (!error) {
instance.userOldStatusText.set(result);
}
});
};
Template.headerRoom.onCreated(function() {
this.currentChannel = (this.data && this.data._id && Rooms.findOne(this.data._id)) || undefined;
this.hasTokenpass = new ReactiveVar(false);
this.userOldStatusText = new ReactiveVar(null);
if (settings.get('API_Tokenpass_URL') !== '') {
Meteor.call('getChannelTokenpass', this.data._id, (error, result) => {
@ -195,4 +236,6 @@ Template.headerRoom.onCreated(function() {
}
});
}
loadUserStatusText();
});

@ -13,7 +13,7 @@
<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">
<input id="name" type="text" value="{{userStatus.name}}" class="rc-input__element" placeholder="{{_ " Name "}}" autocomplete="off">
</div>
</label>
</div>
@ -22,7 +22,7 @@
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Presence"}}</div>
<div class="rc-select">
<select class="rc-select__element" name="statusType">
<select class="rc-select__element" id="statusType">
<option value="">{{_ "None"}}</option>
{{#each options}}
<option value="{{value}}" {{selected}}>{{name}}</option>

@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { userStatus } from './userStatus';
@ -33,22 +34,28 @@ export const updateCustomUserStatus = function(customUserStatusData) {
userStatus.list[newUserStatus.id] = newUserStatus;
};
Meteor.startup(() =>
Meteor.call('listCustomUserStatus', (error, result) => {
if (!result) {
Meteor.startup(() => {
Tracker.autorun(() => {
if (!Meteor.userId()) {
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;
}
})
);
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;
}
});
});
});

@ -2,5 +2,6 @@ import './methods/deleteCustomUserStatus';
import './methods/insertOrUpdateUserStatus';
import './methods/listCustomUserStatus';
import './methods/setUserStatus';
import './methods/getUserStatusText';
import './publications/fullUserStatusData';

@ -0,0 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { getStatusText } from '../../../lib';
Meteor.methods({
getUserStatusText(userId) {
const currentUserId = Meteor.userId();
if (!currentUserId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserStatusText' });
}
return getStatusText(userId);
},
});

@ -4,6 +4,11 @@ import { CustomUserStatus } from '../../../models';
Meteor.methods({
listCustomUserStatus() {
const currentUserId = Meteor.userId();
if (!currentUserId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listCustomUserStatus' });
}
return CustomUserStatus.find({}).fetch();
},
});

@ -2,10 +2,15 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { settings } from '../../../settings';
import { RateLimiter, setStatusMessage } from '../../../lib';
import { RateLimiter, setStatusText } from '../../../lib';
Meteor.methods({
setUserStatus(statusType, statusText) {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setUserStatus' });
}
if (statusType) {
Meteor.call('UserPresence:setDefaultStatus', statusType);
}
@ -19,8 +24,7 @@ Meteor.methods({
});
}
const userId = Meteor.userId();
setStatusMessage(userId, statusText);
setStatusText(userId, statusText);
}
},
});

@ -12,11 +12,14 @@ Meteor.startup(function() {
},
changed(user) {
Session.set(`user_${ user.username }_status`, user.status);
Session.set(`user_${ user.username }_status_text`, user.statusText);
if (user.statusText !== undefined) {
Session.set(`user_${ user.username }_status_text`, user.statusText);
}
RoomManager.updateUserStatus(user, user.status, user.utcOffset);
},
removed(user) {
Session.set(`user_${ user.username }_status`, null);
Session.set(`user_${ user.username }_status_text`, null);
RoomManager.updateUserStatus(user, 'offline', null);
},
});

@ -86,13 +86,13 @@ Tracker.autorun(() => {
});
Meteor.startup(function() {
Notifications.onLogged('user-status', ([_id, username, status]) => {
Notifications.onLogged('user-status', ([_id, username, status, statusText]) => {
// only set after first request completed
if (lastStatusChange) {
lastStatusChange = new Date();
}
saveUser({ _id, username, status: STATUS_MAP[status] }, true);
saveUser({ _id, username, status: STATUS_MAP[status], statusText }, true);
});
Notifications.onLogged('Users:NameChanged', ({ _id, username }) => {

@ -14,6 +14,7 @@ UserPresenceEvents.on('setUserStatus', (user, status/* , statusConnection*/) =>
const {
_id,
username,
statusText,
} = user;
// since this callback can be called by only one instance in the cluster
@ -22,5 +23,6 @@ UserPresenceEvents.on('setUserStatus', (user, status/* , statusConnection*/) =>
_id,
username,
STATUS_MAP[status],
statusText,
]);
});

@ -60,6 +60,7 @@ Meteor.methods({
username: 1,
name: 1,
status: 1,
statusText: 1,
},
sort: {},
};

Loading…
Cancel
Save