[NEW] Added profile field to inform Nickname for users in order to be searchable (#18260)

pull/18277/head
Rodrigo Nascimento 6 years ago committed by GitHub
parent 6429353493
commit 10ea922d89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/api/server/lib/users.js
  2. 4
      app/api/server/v1/users.js
  3. 4
      app/apps/client/gameCenter/invitePlayers.js
  4. 4
      app/discussion/client/views/creationDialog/CreateDiscussion.js
  5. 1
      app/lib/server/functions/getFullUserData.js
  6. 19
      app/lib/server/functions/saveUser.js
  7. 2
      app/lib/server/startup/settings.js
  8. 18
      app/models/server/models/Users.js
  9. 2
      app/models/server/raw/Users.js
  10. 10
      app/ui-account/client/accountProfile.html
  11. 11
      app/ui-account/client/accountProfile.js
  12. 2
      app/ui-flextab/client/tabs/membersList.html
  13. 2
      app/ui-flextab/client/tabs/userInfo.html
  14. 4
      app/ui-flextab/client/tabs/userInfo.js
  15. 11
      app/ui-message/client/popup/messagePopupConfig.js
  16. 2
      app/ui-message/client/popup/messagePopupUser.html
  17. 4
      app/ui/client/components/popupList.html
  18. 2
      app/ui/client/views/app/CreateDirectMessage.js
  19. 1
      app/utils/server/functions/getDefaultUserFields.js
  20. 1
      client/admin/users/AddUser.js
  21. 1
      client/admin/users/EditUser.js
  22. 8
      client/admin/users/UserForm.js
  23. 2
      client/admin/users/UserInfo.js
  24. 4
      client/views/directory/UserTab.js
  25. 2
      packages/rocketchat-i18n/i18n/en.i18n.json
  26. 2
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  27. 2
      server/methods/browseChannels.js
  28. 4
      server/methods/getUsersOfRoom.js
  29. 11
      server/methods/saveUserProfile.js
  30. 1
      server/publications/spotlight.js

@ -13,6 +13,7 @@ export async function findUsersToAutocomplete({ uid, selector }) {
fields: {
name: 1,
username: 1,
nickname: 1,
status: 1,
avatarETag: 1,
},

@ -31,6 +31,7 @@ API.v1.addRoute('users.create', { authRequired: true }, {
username: String,
active: Match.Maybe(Boolean),
bio: Match.Maybe(String),
nickname: Match.Maybe(String),
statusText: Match.Maybe(String),
roles: Match.Maybe(Array),
joinDefaultChannels: Match.Maybe(Boolean),
@ -436,6 +437,7 @@ API.v1.addRoute('users.update', { authRequired: true, twoFactorRequired: true },
password: Match.Maybe(String),
username: Match.Maybe(String),
bio: Match.Maybe(String),
nickname: Match.Maybe(String),
statusText: Match.Maybe(String),
active: Match.Maybe(Boolean),
roles: Match.Maybe(Array),
@ -473,6 +475,7 @@ API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, {
email: Match.Maybe(String),
name: Match.Maybe(String),
username: Match.Maybe(String),
nickname: Match.Maybe(String),
statusText: Match.Maybe(String),
currentPassword: Match.Maybe(String),
newPassword: Match.Maybe(String),
@ -484,6 +487,7 @@ API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, {
email: this.bodyParams.data.email,
realname: this.bodyParams.data.name,
username: this.bodyParams.data.username,
nickname: this.bodyParams.data.nickname,
statusText: this.bodyParams.data.statusText,
newPassword: this.bodyParams.data.newPassword,
typedPassword: this.bodyParams.data.currentPassword,

@ -54,13 +54,13 @@ Template.InvitePlayers.helpers({
roomModifier() {
return (filter, text = '') => {
const f = filter.get();
return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`;
};
},
userModifier() {
return (filter, text = '') => {
const f = filter.get();
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`;
};
},
nameSuggestion() {

@ -72,13 +72,13 @@ Template.CreateDiscussion.helpers({
roomModifier() {
return (filter, text = '') => {
const f = filter.get();
return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`;
};
},
userModifier() {
return (filter, text = '') => {
const f = filter.get();
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`;
};
},
nameSuggestion() {

@ -10,6 +10,7 @@ const logger = new Logger('getFullUserData');
const defaultFields = {
name: 1,
username: 1,
nickname: 1,
status: 1,
utcOffset: 1,
type: 1,

@ -215,6 +215,23 @@ const handleBio = (updateUser, bio) => {
}
};
const handleNickname = (updateUser, nickname) => {
if (nickname) {
if (nickname.trim()) {
if (typeof nickname !== 'string' || nickname.length > 120) {
throw new Meteor.Error('error-invalid-field', 'nickname', {
method: 'saveUserProfile',
});
}
updateUser.$set = updateUser.$set || {};
updateUser.$set.nickname = nickname;
} else {
updateUser.$unset = updateUser.$unset || {};
updateUser.$unset.nickname = 1;
}
}
};
export const saveUser = function(userId, userData) {
validateUserData(userId, userData);
let sendPassword = false;
@ -261,6 +278,7 @@ export const saveUser = function(userId, userData) {
}
handleBio(updateUser, userData.bio);
handleNickname(updateUser, userData.nickname);
Meteor.users.update({ _id }, updateUser);
@ -320,6 +338,7 @@ export const saveUser = function(userId, userData) {
};
handleBio(updateUser, userData.bio);
handleNickname(updateUser, userData.nickname);
if (userData.roles) {
updateUser.$set.roles = userData.roles;

@ -97,7 +97,7 @@ settings.addGroup('Accounts', function() {
type: 'boolean',
public: true,
});
this.add('Accounts_SearchFields', 'username, name, bio', {
this.add('Accounts_SearchFields', 'username, name, bio, nickname', {
type: 'string',
});
this.add('Accounts_Directory_DefaultView', 'channels', {

@ -30,7 +30,8 @@ export class Users extends Base {
this.tryEnsureIndex({ roles: 1 }, { sparse: 1 });
this.tryEnsureIndex({ name: 1 });
this.tryEnsureIndex({ bio: 1 });
this.tryEnsureIndex({ bio: 1 }, { sparse: 1 });
this.tryEnsureIndex({ nickname: 1 }, { sparse: 1 });
this.tryEnsureIndex({ createdAt: 1 });
this.tryEnsureIndex({ lastLogin: 1 });
this.tryEnsureIndex({ status: 1 });
@ -1151,6 +1152,21 @@ export class Users extends Base {
return this.update(_id, update);
}
setNickname(_id, nickname = '') {
const update = {
...nickname.trim() ? {
$set: {
nickname,
},
} : {
$unset: {
nickname: 1,
},
},
};
return this.update(_id, update);
}
clearSettings(_id) {
const update = {
$set: {

@ -126,6 +126,8 @@ export class UsersRaw extends BaseRaw {
username: termRegex,
}, {
name: termRegex,
}, {
nickname: termRegex,
}],
active: true,
type: {

@ -139,6 +139,16 @@
</label>
</div>
</div>
<div class="rc-form-group">
<div class="rc-input rc-w100 padded">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Nickname"}}</div>
<div class="rc-input__wrapper">
<input type="text" class="rc-input__element" name="nickname" id="nickname" maxlength="120" placeholder="{{_ "Nickname_Placeholder" }}" value="{{get 'nickname'}}"/>
</div>
</label>
</div>
</div>
<div class="rc-form-group rc-grid">
{{#with canChange=allowEmailChange}}
<div class="rc-input rc-w50 padded {{#if emailInvalid}}rc-input--error{{/if}}">

@ -123,6 +123,7 @@ Template.accountProfile.helpers({
const statusType = instance.statusType.get();
const statusText = instance.fields.get('statusText');
const bio = instance.fields.get('bio');
const nickname = instance.fields.get('nickname');
const username = instance.username.get();
const password = instance.password.get();
const confirmationPassword = instance.confirmationPassword.get();
@ -141,7 +142,7 @@ Template.accountProfile.helpers({
}
}
if (!avatar && user.bio === bio && user.name === realname && user.username === username && getUserEmailAddress(user) === email && statusText === user.statusText && !password && statusType === user.status) {
if (!avatar && user.bio === bio && user.nickname === nickname && user.name === realname && user.username === username && getUserEmailAddress(user) === email && statusText === user.statusText && !password && statusType === user.status) {
return ret;
}
@ -212,6 +213,7 @@ Template.accountProfile.onCreated(function() {
this.fields = new ReactiveDict({
statusText: user.statusText,
bio: user.bio,
nickname: user.nickname,
});
const self = this;
@ -302,9 +304,12 @@ Template.accountProfile.onCreated(function() {
data.statusText = s.trim(self.fields.get('statusText'));
}
if (s.trim(self.fields.get('bio')) !== user.statusText) {
if (s.trim(self.fields.get('bio')) !== user.bio) {
data.bio = s.trim(self.fields.get('bio'));
}
if (s.trim(self.fields.get('nickname')) !== user.nickname) {
data.nickname = s.trim(self.fields.get('nickname'));
}
if (self.statusType.get() !== user.statusType) {
data.statusType = self.statusType.get();
}
@ -438,7 +443,7 @@ Template.accountProfile.events({
instance.confirmationPassword.set('');
}
},
'input [name=bio], input [name=statusText]'(e, instance) {
'input [name=bio], input [name=statusText], input [name=nickname]'(e, instance) {
instance.fields.set(e.target.name, e.target.value);
},
'input [name=confirmation-password]'(e, instance) {

@ -37,7 +37,7 @@
{{> avatar url=avatarUrl}}
<div class="rc-member-list__username">
{{# userPresence uid=user._id}}<div class="rc-member-list__status rc-member-list__status--{{status}}"></div>{{/userPresence}}
{{ignored}} {{displayName}} {{utcOffset}}
{{ignored}} {{displayName}}{{#if user.nickname}} ({{user.nickname}}){{/if}} {{utcOffset}}
</div>
{{> icon user=. block="rc-member-list__menu js-more" icon="menu" }}
</li>

@ -29,7 +29,7 @@
<div class="rc-user-info__avatar">
{{> avatar username=username}}
</div>
<h3 title="{{name}}" class="rc-user-info__name">{{name}}</h3>
<h3 title="{{name}}" class="rc-user-info__name">{{name}}{{#if nickname}} ({{nickname}}){{/if}}</h3>
{{#if username}}<p class="rc-user-info__username">@{{username}}</p>{{/if}}
{{# userPresence uid=uid}}<span class="rc-header__status rc-user-info__status">
<div class="rc-header__status-bullet rc-header__status-bullet--{{userStatus}}" title="{{_ userStatus}}"></div>

@ -193,6 +193,10 @@ Template.userInfo.helpers({
const user = Template.instance().user.get();
return user.bio && user.bio.trim();
},
nickname() {
const user = Template.instance().user.get();
return user.nickname?.trim();
},
bio() {
const user = Template.instance().user.get();
return Markdown.parse(user.bio);

@ -67,11 +67,12 @@ const fetchUsersFromServer = _.throttle(async (filterText, records, rid, cb) =>
users
.slice(0, 5)
.forEach(({ username, name, status, avatarETag }) => {
.forEach(({ username, nickname, name, status, avatarETag }) => {
if (records.length < 5) {
records.push({
_id: username,
username,
nickname,
name,
status,
avatarETag,
@ -260,6 +261,7 @@ Template.messagePopupConfig.helpers({
{
fields: {
username: 1,
nickname: 1,
name: 1,
status: 1,
},
@ -267,9 +269,10 @@ Template.messagePopupConfig.helpers({
},
)
.fetch()
.map(({ username, name, status }) => ({
.map(({ username, name, status, nickname }) => ({
_id: username,
username,
nickname,
name,
status,
sort: 1,
@ -301,15 +304,17 @@ Template.messagePopupConfig.helpers({
{
fields: {
username: 1,
nickname: 1,
name: 1,
status: 1,
},
limit: 5 - usernamesAlreadyFetched.length,
})
.fetch()
.map(({ username, name, status }) => ({
.map(({ username, name, status, nickname }) => ({
_id: username,
username,
nickname,
name,
status,
sort: 1,

@ -3,5 +3,5 @@
<div class="popup-user-status border-transparent-dark popup-user-status-{{status}}"></div>
<div class="popup-user-avatar" style="background-image:url({{avatarUrlFromUsername username avatarETag}});"></div>
{{/unless}}
<strong>{{username}}</strong> {{name}}
<strong>{{username}}</strong> {{name}}{{#if nickname}} ({{nickname}}){{/if}}
</template>

@ -24,9 +24,9 @@
{{>avatar username=item.username}}
</span>
{{#if showRealNames}}
<span class="rc-popup-list__item-name">{{item.name}} ({{{modifier item.username}}})</span>
<span class="rc-popup-list__item-name">{{{modifier item.name}}}{{#if item.nickname}} ({{{modifier item.nickname}}}){{/if}} ({{{modifier item.username}}})</span>
{{else}}
<span class="rc-popup-list__item-name">{{{modifier item.username}}}</span>
<span class="rc-popup-list__item-name">{{{modifier item.username}}}{{#if item.nickname}} ({{item.nickname}}){{/if}}</span>
{{/if}}
</li>
</template>

@ -43,7 +43,7 @@ Template.CreateDirectMessage.helpers({
userModifier() {
return (filter, text = '') => {
const f = filter.get();
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`;
};
},
nameSuggestion() {

@ -1,6 +1,7 @@
export const getDefaultUserFields = () => ({
name: 1,
username: 1,
nickname: 1,
emails: 1,
status: 1,
statusDefault: 1,

@ -27,6 +27,7 @@ export function AddUser({ roles, ...props }) {
username: '',
statusText: '',
bio: '',
nickname: '',
email: '',
password: '',
verified: false,

@ -34,6 +34,7 @@ const getInitialValue = (data) => ({
username: data.username,
status: data.status,
bio: data.bio ?? '',
nickname: data.nickname ?? '',
email: (data.emails && data.emails[0].address) || '',
verified: (data.emails && data.emails[0].verified) || false,
setRandomPassword: false,

@ -17,6 +17,7 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app
verified,
statusText,
bio,
nickname,
password,
setRandomPassword,
requirePasswordChange,
@ -33,6 +34,7 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app
handleVerified,
handleStatusText,
handleBio,
handleNickname,
handlePassword,
handleSetRandomPassword,
handleRequirePasswordChange,
@ -79,6 +81,12 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app
<TextAreaInput rows={3} flexGrow={1} value={bio} onChange={handleBio} addon={<Icon name='edit' size='x20' alignSelf='center'/>}/>
</Field.Row>
</Field>, [bio, handleBio, t])}
{useMemo(() => <Field>
<Field.Label>{t('Nickname')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={nickname} onChange={handleNickname} addon={<Icon name='edit' size='x20' alignSelf='center'/>}/>
</Field.Row>
</Field>, [nickname, handleNickname, t])}
{useMemo(() => <Field>
<Field.Label>{t('Password')}</Field.Label>
<Field.Row>

@ -63,7 +63,7 @@ export function UserInfo({ data, onChange, ...props }) {
<Box display='flex' flexDirection='column' alignItems='center' flexShrink={0} withTruncatedText>
<Margins block='x2' inline='auto'>
<Avatar size={'x120'} title={data.username} url={avatarUrl}/>
<Box fontScale='h1' withTruncatedText>{data.name || data.username}</Box>
<Box fontScale='h1' withTruncatedText>{data.name || data.username}{data.nickname && ` (${ data.nickname })`}</Box>
{!!data.name && <Box fontScale='p1' color='hint' withTruncatedText>@{data.username}</Box>}
<Box fontScale='p1' color='hint' withTruncatedText>{data.status}</Box>
</Margins>

@ -72,7 +72,7 @@ function UserTable({
const formatDate = useFormatDate();
const renderRow = useCallback(({ createdAt, emails, _id, username, name, domain, bio, avatarETag }) => {
const renderRow = useCallback(({ createdAt, emails, _id, username, name, domain, bio, nickname, avatarETag }) => {
const avatarUrl = getUserAvatarURL(username, avatarETag);
return <Table.Row key={_id} onKeyDown={onClick(username)} onClick={onClick(username)} tabIndex={0} role='link' action>
@ -84,7 +84,7 @@ function UserTable({
</Flex.Item>
<Box style={style} grow={1} mi='x8'>
<Box display='flex'>
<Box fontScale='p2' style={style}>{name || username}</Box> <Box mi='x4'/> <Box fontScale='p1' color='hint' style={style}>{username}</Box>
<Box fontScale='p2' style={style}>{name || username}{nickname && ` (${ nickname })`}</Box> <Box mi='x4'/> <Box fontScale='p1' color='hint' style={style}>{username}</Box>
</Box>
<Box fontScale='p1' color='hint' style={style}> {bio} </Box>
</Box>

@ -1688,6 +1688,8 @@
"Flags": "Flags",
"Follow_message": "Follow Message",
"Following": "Following",
"Nickname": "Nickname",
"Nickname_Placeholder": "Enter your nickname...",
"Not_Following": "Not Following",
"Follow_social_profiles": "Follow our social profiles, fork us on github and share your thoughts about the rocket.chat app on our trello board.",
"Fonts": "Fonts",

@ -2298,6 +2298,8 @@
"New_visitor_navigation": "Nova Navegação: __history__",
"Newer_than": "Mais recente que",
"Newer_than_may_not_exceed_Older_than": "\"Mais recente que\" não pode exceder \"Mais antigo que\"",
"Nickname": "Apelido",
"Nickname_Placeholder": "Digite seu apelido...",
"No_Limit": "Sem limite",
"No_available_agents_to_transfer": "Nenhum agente disponível para transferir",
"No_channel_with_name_%s_was_found": "Nenhum canal com nome <strong>\"%s\"</strong> foi encontrado!",

@ -125,6 +125,7 @@ Meteor.methods({
fields: {
username: 1,
name: 1,
nickname: 1,
bio: 1,
createdAt: 1,
emails: 1,
@ -157,6 +158,7 @@ Meteor.methods({
username: user.username,
name: user.name,
bio: user.bio,
nickname: user.nickname,
emails: user.emails,
federation: user.federation,
isRemote: true,

@ -25,12 +25,13 @@ function findUsers({ rid, status, skip, limit, filter = '' }) {
'u._id': 1,
'u.name': 1,
'u.username': 1,
'u.nickname': 1,
'u.status': 1,
'u.avatarETag': 1,
},
},
...status ? [{ $match: { 'u.status': status } }] : [],
...filter.trim() ? [{ $match: { $or: [{ 'u.name': regex }, { 'u.username': regex }] } }] : [],
...filter.trim() ? [{ $match: { $or: [{ 'u.name': regex }, { 'u.username': regex }, { 'u.nickname': regex }] } }] : [],
{
$sort: {
[settings.get('UI_Use_Real_Name') ? 'u.name' : 'u.username']: 1,
@ -43,6 +44,7 @@ function findUsers({ rid, status, skip, limit, filter = '' }) {
_id: { $arrayElemAt: ['$u._id', 0] },
name: { $arrayElemAt: ['$u.name', 0] },
username: { $arrayElemAt: ['$u.username', 0] },
nickname: { $arrayElemAt: ['$u.nickname', 0] },
avatarETag: { $arrayElemAt: ['$u.avatarETag', 0] },
},
},

@ -46,7 +46,7 @@ Meteor.methods({
Meteor.call('setUserStatus', settings.statusType, null);
}
if (settings.bio) {
if (settings.bio != null) {
if (typeof settings.bio !== 'string' || settings.bio.length > 260) {
throw new Meteor.Error('error-invalid-field', 'bio', {
method: 'saveUserProfile',
@ -55,6 +55,15 @@ Meteor.methods({
Users.setBio(user._id, settings.bio.trim());
}
if (settings.nickname != null) {
if (typeof settings.nickname !== 'string' || settings.nickname.length > 120) {
throw new Meteor.Error('error-invalid-field', 'nickname', {
method: 'saveUserProfile',
});
}
Users.setNickname(user._id, settings.nickname.trim());
}
if (settings.email) {
if (!compareUserPassword(user, { sha256: settings.typedPassword })) {
throw new Meteor.Error('error-invalid-password', 'Invalid password', {

@ -58,6 +58,7 @@ Meteor.methods({
limit: 5,
fields: {
username: 1,
nickname: 1,
name: 1,
status: 1,
statusText: 1,

Loading…
Cancel
Save