[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
parent
b954bb8d3c
commit
fbb47b6783
@ -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'); |
||||
}, |
||||
}); |
@ -0,0 +1,10 @@ |
||||
import { Base } from './_Base'; |
||||
|
||||
class CustomUserStatus extends Base { |
||||
constructor() { |
||||
super(); |
||||
this._initModel('custom_user_status'); |
||||
} |
||||
} |
||||
|
||||
export default new CustomUserStatus(); |
@ -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(); |
@ -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'; |
@ -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); |
||||
}); |
@ -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(); |
||||
}); |
@ -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"> </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); |
||||
}); |
Loading…
Reference in new issue