[NEW] Rewrite admin pages (#17388)

Co-authored-by: Maria Eduarda Cunha <42151808+mariaeduardacunha@users.noreply.github.com>
Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>
Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com>
Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>
Co-authored-by: Martin Schoeler <martin.schoeler@rocket.chat>
pull/17533/head^2
Guilherme Gazzo 6 years ago committed by GitHub
parent 0b1b0c27bc
commit ca665fae2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      .storybook/.babelrc
  2. 4
      .storybook/addons.js
  3. 20
      .storybook/babel.config.js
  4. 20
      .storybook/config.js
  5. 11
      .storybook/main.js
  6. 16
      .storybook/mocks/meteor.js
  7. 7
      .storybook/preview.js
  8. 38
      .storybook/webpack.config.js
  9. 28
      app/api/server/lib/rooms.js
  10. 43
      app/api/server/v1/rooms.js
  11. 3
      app/api/server/v1/users.js
  12. 2
      app/apps/client/orchestrator.js
  13. 2
      app/apps/client/routes.js
  14. 2
      app/authorization/client/route.js
  15. 2
      app/authorization/client/startup.js
  16. 2
      app/channel-settings/client/views/Multiselect.js
  17. 2
      app/chatpal-search/client/route.js
  18. 2
      app/cloud/client/index.js
  19. 111
      app/custom-sounds/assets/stylesheets/customSoundsAdmin.css
  20. 7
      app/custom-sounds/client/admin/adminSoundEdit.html
  21. 7
      app/custom-sounds/client/admin/adminSoundInfo.html
  22. 78
      app/custom-sounds/client/admin/adminSounds.html
  23. 176
      app/custom-sounds/client/admin/adminSounds.js
  24. 11
      app/custom-sounds/client/admin/route.js
  25. 25
      app/custom-sounds/client/admin/soundEdit.html
  26. 155
      app/custom-sounds/client/admin/soundEdit.js
  27. 19
      app/custom-sounds/client/admin/soundInfo.html
  28. 118
      app/custom-sounds/client/admin/soundInfo.js
  29. 11
      app/custom-sounds/client/admin/startup.js
  30. 8
      app/custom-sounds/client/admin/views.js
  31. 3
      app/custom-sounds/client/index.js
  32. 6
      app/custom-sounds/client/lib/CustomSounds.js
  33. 2
      app/custom-sounds/server/methods/insertOrUpdateSound.js
  34. 132
      app/emoji-custom/assets/stylesheets/emojiCustomAdmin.css
  35. 76
      app/emoji-custom/client/admin/adminEmoji.html
  36. 141
      app/emoji-custom/client/admin/adminEmoji.js
  37. 7
      app/emoji-custom/client/admin/adminEmojiEdit.html
  38. 7
      app/emoji-custom/client/admin/adminEmojiInfo.html
  39. 31
      app/emoji-custom/client/admin/emojiEdit.html
  40. 158
      app/emoji-custom/client/admin/emojiEdit.js
  41. 23
      app/emoji-custom/client/admin/emojiInfo.html
  42. 126
      app/emoji-custom/client/admin/emojiInfo.js
  43. 5
      app/emoji-custom/client/admin/emojiPreview.html
  44. 11
      app/emoji-custom/client/admin/route.js
  45. 2
      app/emoji-custom/client/admin/startup.js
  46. 9
      app/emoji-custom/client/admin/views.js
  47. 2
      app/emoji-custom/client/index.js
  48. 2
      app/federation/client/admin/dashboard.js
  49. 35
      app/importer/client/ImporterWebsocketReceiver.js
  50. 115
      app/importer/client/admin/importOperationSummary.html
  51. 41
      app/importer/client/functions/showImporterException.js
  52. 3
      app/importer/client/index.js
  53. 25
      app/importer/client/routes.js
  54. 2
      app/integrations/client/route.js
  55. 2
      app/integrations/client/startup.js
  56. 80
      app/invites/client/admin/adminInvites.html
  57. 93
      app/invites/client/admin/adminInvites.js
  58. 11
      app/invites/client/admin/route.js
  59. 11
      app/invites/client/admin/startup.js
  60. 2
      app/invites/client/index.js
  61. 2
      app/lib/lib/roomTypes/direct.js
  62. 34
      app/logger/client/ansispan.js
  63. 1
      app/logger/client/index.js
  64. 33
      app/logger/client/viewLogs.js
  65. 29
      app/logger/client/views/viewLogs.css
  66. 17
      app/logger/client/views/viewLogs.html
  67. 121
      app/logger/client/views/viewLogs.js
  68. 2
      app/mail-messages/client/startup.js
  69. 2
      app/oauth2-server-config/client/admin/route.js
  70. 2
      app/oauth2-server-config/client/admin/startup.js
  71. 47
      app/theme/client/imports/general/base_old.css
  72. 7
      app/theme/client/imports/general/rtl.css
  73. 17
      app/ui-admin/client/components/NotAuthorizedPage.js
  74. 22
      app/ui-admin/client/components/info/BuildEnvironmentSection.js
  75. 22
      app/ui-admin/client/components/info/CommitSection.js
  76. 16
      app/ui-admin/client/components/info/DescriptionList.js
  77. 29
      app/ui-admin/client/components/info/RocketChatSection.js
  78. 31
      app/ui-admin/client/components/info/RuntimeEnvironmentSection.js
  79. 51
      app/ui-admin/client/components/info/UsageSection.js
  80. 43
      app/ui-admin/client/components/info/formatters.js
  81. 10
      app/ui-admin/client/hooks/useAdminSideNav.js
  82. 102
      app/ui-admin/client/rooms/adminRoomInfo.html
  83. 304
      app/ui-admin/client/rooms/adminRoomInfo.js
  84. 78
      app/ui-admin/client/rooms/adminRooms.html
  85. 210
      app/ui-admin/client/rooms/adminRooms.js
  86. 20
      app/ui-admin/client/rooms/channelSettingsDefault.html
  87. 84
      app/ui-admin/client/rooms/channelSettingsDefault.js
  88. 15
      app/ui-admin/client/rooms/channelSettingsFeatured.html
  89. 65
      app/ui-admin/client/rooms/channelSettingsFeatured.js
  90. 7
      app/ui-admin/client/rooms/views.js
  91. 67
      app/ui-admin/client/routes.js
  92. 10
      app/ui-admin/client/sidebarItems.js
  93. 31
      app/ui-admin/client/users/adminInviteUser.html
  94. 54
      app/ui-admin/client/users/adminInviteUser.js
  95. 3
      app/ui-admin/client/users/adminUserEdit.html
  96. 9
      app/ui-admin/client/users/adminUserInfo.html
  97. 101
      app/ui-admin/client/users/adminUsers.html
  98. 183
      app/ui-admin/client/users/adminUsers.js
  99. 6
      app/ui-admin/client/users/views.js
  100. 3
      app/ui-message/client/blocks/MessageBlock.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,19 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true,
"useBuiltIns": "usage",
"corejs": "3",
"modules": "commonjs"
}
],
"@babel/preset-react",
"@babel/preset-flow"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining"
]
}

@ -1,4 +0,0 @@
import '@storybook/addon-actions/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-links/register';
import '@storybook/addon-viewport/register';

@ -0,0 +1,20 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
shippedProposals: true,
useBuiltIns: 'usage',
corejs: '3',
modules: 'commonjs',
},
],
'@babel/preset-react',
'@babel/preset-flow',
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
};

@ -1,20 +0,0 @@
import { withKnobs } from '@storybook/addon-knobs';
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults';
import { addDecorator, addParameters, configure } from '@storybook/react';
import { rocketChatDecorator } from './mocks/decorators';
addParameters({
viewport: {
viewports: MINIMAL_VIEWPORTS,
},
});
addDecorator(rocketChatDecorator);
addDecorator(withKnobs);
configure([
require.context('../app', true, /\.stories\.js$/),
require.context('../client', true, /\.stories\.js$/),
require.context('../ee/app', true, /\.stories\.js$/),
], module);

@ -0,0 +1,11 @@
module.exports = {
stories: [
'../app/**/*.stories.js',
'../client/**/*.stories.js',
'../ee/app/**/*.stories.js',
],
addons: [
'@storybook/addon-actions',
'@storybook/addon-knobs',
],
};

@ -4,7 +4,10 @@ export const Meteor = {
_localStorage: window.localStorage,
absoluteUrl: () => {},
userId: () => {},
Streamer: () => {},
Streamer: () => ({
on: () => {},
removeListener: () => {},
}),
startup: () => {},
methods: () => {},
call: () => {},
@ -31,10 +34,13 @@ export const Mongo = {
}),
};
export const ReactiveVar = () => ({
get: () => {},
set: () => {},
});
export const ReactiveVar = (val) => {
let currentVal = val;
return {
get: () => currentVal,
set: (val) => { currentVal = val; },
};
};
export const ReactiveDict = () => ({
get: () => {},

@ -0,0 +1,7 @@
import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator } from '@storybook/react';
import { rocketChatDecorator } from './mocks/decorators';
addDecorator(rocketChatDecorator);
addDecorator(withKnobs);

@ -31,15 +31,35 @@ module.exports = async ({ config }) => {
use: '@settlin/spacebars-loader',
});
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/^meteor/,
require.resolve('./mocks/meteor.js'),
));
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/\.\/server\/index.js/,
require.resolve('./mocks/empty.js'),
));
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: 'ts-loader',
options: {
compilerOptions: {
noEmit: false,
},
},
},
{
loader: 'react-docgen-typescript-loader',
},
],
});
config.resolve.extensions.push('.ts', '.tsx');
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/^meteor/,
require.resolve('./mocks/meteor.js'),
),
new webpack.NormalModuleReplacementPlugin(
/\/server(\/index.js)$/,
require.resolve('./mocks/empty.js'),
),
);
config.mode = 'development';
config.optimization.usedExports = true;

@ -56,6 +56,34 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of
};
}
export async function findAdminRoom({ uid, rid }) {
if (!await hasPermissionAsync(uid, 'view-room-administration')) {
throw new Error('error-not-authorized');
}
const fields = {
prid: 1,
fname: 1,
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
usersCount: 1,
muted: 1,
unmuted: 1,
ro: 1,
default: 1,
favorite: 1,
featured: 1,
topic: 1,
msgs: 1,
archived: 1,
tokenpass: 1,
};
return Rooms.findOneById(rid, { fields });
}
export async function findChannelAndPrivateAutocomplete({ uid, selector }) {
if (!await hasPermissionAsync(uid, 'view-other-user-channels')) {
return { items: [] };

@ -4,7 +4,7 @@ import Busboy from 'busboy';
import { FileUpload } from '../../../file-upload';
import { Rooms, Messages } from '../../../models';
import { API } from '../api';
import { findAdminRooms, findChannelAndPrivateAutocomplete } from '../lib/rooms';
import { findAdminRooms, findChannelAndPrivateAutocomplete, findAdminRoom } from '../lib/rooms';
function findRoomByIdOrName({ params, checkedArchived = true }) {
if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) {
@ -299,6 +299,22 @@ API.v1.addRoute('rooms.adminRooms', { authRequired: true }, {
},
});
API.v1.addRoute('rooms.adminRooms.getRoom', { authRequired: true }, {
get() {
const { rid } = this.requestParams();
const room = Promise.await(findAdminRoom({
uid: this.userId,
rid,
}));
if (!room) {
return API.v1.failure('not-allowed', 'Not Allowed');
}
return API.v1.success(room);
},
});
API.v1.addRoute('rooms.autocomplete.channelAndPrivate', { authRequired: true }, {
get() {
const { selector } = this.queryParams;
@ -312,3 +328,28 @@ API.v1.addRoute('rooms.autocomplete.channelAndPrivate', { authRequired: true },
})));
},
});
API.v1.addRoute('rooms.saveRoomSettings', { authRequired: true }, {
post() {
const { rid, ...params } = this.bodyParams;
const result = Meteor.runAsUser(this.userId, () => Meteor.call('saveRoomSettings', rid, params));
return API.v1.success({ rid: result.rid });
},
});
API.v1.addRoute('rooms.changeArchivationState', { authRequired: true }, {
post() {
const { rid, action } = this.bodyParams;
let result;
if (action === 'archive') {
result = Meteor.runAsUser(this.userId, () => Meteor.call('archiveRoom', rid));
} else {
result = Meteor.runAsUser(this.userId, () => Meteor.call('unarchiveRoom', rid));
}
return API.v1.success({ result });
},
});

@ -30,6 +30,8 @@ API.v1.addRoute('users.create', { authRequired: true }, {
password: String,
username: String,
active: Match.Maybe(Boolean),
bio: Match.Maybe(String),
statusText: Match.Maybe(String),
roles: Match.Maybe(Array),
joinDefaultChannels: Match.Maybe(Boolean),
requirePasswordChange: Match.Maybe(Boolean),
@ -431,6 +433,7 @@ API.v1.addRoute('users.update', { authRequired: true, twoFactorRequired: true },
name: Match.Maybe(String),
password: Match.Maybe(String),
username: Match.Maybe(String),
bio: Match.Maybe(String),
statusText: Match.Maybe(String),
active: Match.Maybe(Boolean),
roles: Match.Maybe(Array),

@ -4,7 +4,7 @@ import toastr from 'toastr';
import { AppWebsocketReceiver } from './communication';
import { APIClient } from '../../utils';
import { registerAdminSidebarItem } from '../../ui-admin/client';
import { registerAdminSidebarItem } from '../../../client/admin';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { hasAtLeastOnePermission } from '../../authorization';
import { handleI18nResources } from './i18n';

@ -1,7 +1,7 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../ui-admin/client';
import { registerAdminRoute } from '../../../client/admin';
import { Apps } from './orchestrator';
registerAdminRoute('/apps/what-is-it', {

@ -1,6 +1,6 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../ui-admin/client';
import { registerAdminRoute } from '../../../client/admin';
import { t } from '../../utils/client';
registerAdminRoute('/permissions', {

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { hasAtLeastOnePermission } from './hasPermission';
import { registerAdminSidebarItem } from '../../ui-admin/client';
import { registerAdminSidebarItem } from '../../../client/admin';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { APIClient } from '../../utils/client';
import { Roles } from '../../models/client';

@ -4,7 +4,7 @@ import { createTemplateForComponent } from '../../../../client/reactAdapters';
createTemplateForComponent(
'Multiselect',
() => import('../../../ui-admin/client/components/settings/inputs/MultiSelectSettingInput'),
() => import('../../../../client/admin/settings/inputs/MultiSelectSettingInput'),
{
// eslint-disable-next-line new-cap
renderContainerView: () => HTML.DIV({ class: 'rc-multiselect', style: 'display: flex;' }),

@ -1,6 +1,6 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../ui-admin/client';
import { registerAdminRoute } from '../../../client/admin';
import { t } from '../../utils';
registerAdminRoute('/chatpal', {

@ -4,7 +4,7 @@ import './admin/cloudRegisterManually';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/client';
import { registerAdminRoute, registerAdminSidebarItem } from '../../../client/admin';
import { hasAtLeastOnePermission } from '../../authorization';
registerAdminRoute('/cloud', {

@ -1,111 +0,0 @@
.sound-info {
& .icon-play-circled {
cursor: pointer;
}
}
.sound-view {
z-index: 15;
overflow-x: hidden;
overflow-y: auto;
& .thumb {
width: 100%;
height: 350px;
padding: 20px;
}
& nav {
padding: 0 20px;
}
& .info {
padding: 0 20px;
white-space: normal;
& h3 {
overflow: hidden;
width: 100%;
margin: 8px 0;
user-select: text;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 24px;
line-height: 27px;
& i::after {
display: inline-block;
width: 8px;
height: 8px;
content: " ";
vertical-align: middle;
border-radius: 4px;
}
}
& p {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
font-size: 12px;
font-weight: 300;
line-height: 18px;
}
}
& .edit-form {
padding: 20px 20px 0;
white-space: normal;
& h3 {
margin-bottom: 8px;
font-size: 24px;
line-height: 22px;
}
& p {
font-size: 12px;
font-weight: 300;
line-height: 18px;
}
& > .input-line {
margin-top: 20px;
}
& nav {
padding: 0;
&.buttons {
margin-top: 2em;
}
}
& .form-divisor {
height: 9px;
margin: 2em 0;
text-align: center;
& > span {
padding: 0 1em;
}
}
}
& .room-info-content > div {
margin: 0 0 20px;
}
}

@ -1,7 +0,0 @@
<template name="adminSoundEdit">
<div class="content">
<div class="sound-view">
{{> soundEdit .}}
</div>
</div>
</template>

@ -1,7 +0,0 @@
<template name="adminSoundInfo">
<div class="content">
<div class="sound-view">
{{> soundInfo .}}
</div>
</div>
</template>

@ -1,78 +0,0 @@
<template name="adminSounds">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Custom_Sounds"}}
<div class="content">
{{#requiresPermission 'manage-sounds'}}
<form class="search-form" role="form">
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{#if isLoading}}
{{> loading }}
{{else}}
{{> icon block="rc-input__icon-svg" icon="magnifier" }}
{{/if}}
</div>
<input id="sound-filter" type="text" class="rc-input__element"
placeholder="{{_ "Search"}}" autofocus dir="auto">
</div>
</form>
<div class="results">
{{{_ "Showing_results" customsounds.length}}}
</div>
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr>
<th width="95%">
<div class="table-fake-th">{{_ "Name"}}</div>
</th>
<th width="5%">
<div class="table-fake-th">{{_ "Action"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each customsounds}}
<tr>
<td width="80%">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">
{{name}}
</span>
</div>
</div>
</td>
<td width="20%">
<div class="rc-table-wrapper">
{{#if isPlaying _id}}
{{>icon _id=_id icon="pause" block="icon-pause-circled"}}
{{else}}
{{>icon _id=_id icon="play" block="icon-play-circled"}}
{{/if}}
{{>icon _id=_id icon="ban" block="icon-reset-circled"}}
</div>
</td>
</tr>
{{else}}
{{# with searchText}}
<tr class="table-no-click">
<td>{{_ "No_results_found_for"}} {{.}}</td>
</tr>
{{/with}}
{{/each}}
{{#if isLoading}}
<tr class="table-no-click">
<td class="table-loading-td">{{> loading}}</td>
</tr>
{{/if}}
</tbody>
{{/table}}
{{/requiresPermission}}
</div>
</section>
{{#with flexData}}
{{> flexTabBar}}
{{/with}}
</div>
</template>

@ -1,176 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import _ from 'underscore';
import { RocketChatTabBar, SideNav, TabBar } from '../../../ui-utils';
import { CustomSounds } from '../lib/CustomSounds';
import { APIClient } from '../../../utils/client';
const LIST_SIZE = 50;
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500;
Template.adminSounds.helpers({
searchText() {
const instance = Template.instance();
return instance.filter && instance.filter.get();
},
isPlaying(_id) {
return Template.instance().isPlayingId.get() === _id;
},
customsounds() {
return Template.instance().sounds.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
flexData() {
return {
tabBar: Template.instance().tabBar,
data: Template.instance().tabBarData.get(),
};
},
onTableScroll() {
const instance = Template.instance();
return function(currentTarget) {
if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) {
return;
}
const sounds = instance.sounds.get();
if (instance.total.get() > sounds.length) {
instance.offset.set(instance.offset.get() + LIST_SIZE);
}
};
},
onTableItemClick() {
const instance = Template.instance();
return function(item) {
instance.tabBarData.set({
sound: instance.sounds.get().find((sound) => sound._id === item._id),
onSuccess: instance.onSuccessCallback,
});
instance.tabBar.showGroup('custom-sounds-selected');
instance.tabBar.open('admin-sound-info');
};
},
});
Template.adminSounds.onCreated(function() {
const instance = this;
this.sounds = new ReactiveVar([]);
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.query = new ReactiveVar({});
this.isLoading = new ReactiveVar(false);
this.filter = new ReactiveVar('');
this.isPlayingId = new ReactiveVar('');
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
this.tabBarData = new ReactiveVar();
TabBar.addButton({
groups: ['custom-sounds', 'custom-sounds-selected'],
id: 'add-sound',
i18nTitle: 'Custom_Sound_Add',
icon: 'plus',
template: 'adminSoundEdit',
order: 1,
});
TabBar.addButton({
groups: ['custom-sounds-selected'],
id: 'admin-sound-info',
i18nTitle: 'Custom_Sound_Info',
icon: 'customize',
template: 'adminSoundInfo',
order: 2,
});
this.onSuccessCallback = () => {
this.offset.set(0);
return this.loadSounds(this.query.get(), this.offset.get());
};
this.tabBarData.set({
onSuccess: instance.onSuccessCallback,
});
this.loadSounds = _.debounce(async (query, offset) => {
this.isLoading.set(true);
const { sounds, total } = await APIClient.v1.get(`custom-sounds.list?count=${ LIST_SIZE }&offset=${ offset }&query=${ JSON.stringify(query) }`);
this.total.set(total);
if (offset === 0) {
this.sounds.set(sounds);
} else {
this.sounds.set(this.sounds.get().concat(sounds));
}
this.isLoading.set(false);
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS);
this.autorun(() => {
const filter = this.filter.get() && this.filter.get().trim();
const offset = this.offset.get();
if (filter) {
const regex = { $regex: filter, $options: 'i' };
return this.loadSounds({ name: regex }, offset);
}
return this.loadSounds({}, offset);
});
});
Template.adminSounds.onRendered(() =>
Tracker.afterFlush(function() {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}),
);
Template.adminSounds.events({
'keydown #sound-filter'(e) {
// stop enter key
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
}
},
'keyup #sound-filter'(e, t) {
e.stopPropagation();
e.preventDefault();
t.filter.set(e.currentTarget.value);
t.offset.set(0);
},
'click .icon-play-circled'(e, t) {
e.preventDefault();
e.stopPropagation();
CustomSounds.play(this._id);
const audio = document.getElementById(t.isPlayingId.get());
if (audio) {
audio.pause();
}
document.getElementById(this._id).onended = () => {
t.isPlayingId.set('');
this.onended = null;
};
t.isPlayingId.set(this._id);
},
'click .icon-pause-circled'(e, t) {
e.preventDefault();
e.stopPropagation();
const audio = document.getElementById(this._id);
if (audio && !audio.paused) {
audio.pause();
}
t.isPlayingId.set('');
},
'click .icon-reset-circled'(e) {
e.preventDefault();
e.stopPropagation();
const audio = document.getElementById(this._id);
if (audio) {
audio.currentTime = 0;
}
},
});

@ -1,11 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../ui-admin/client';
registerAdminRoute('/custom-sounds', {
name: 'custom-sounds',
async action(/* params*/) {
await import('./views');
BlazeLayout.render('main', { center: 'adminSounds' });
},
});

@ -1,25 +0,0 @@
<template name="soundEdit">
{{#requiresPermission 'manage-sounds'}}
<div class="about clearfix">
<form class="edit-form" autocomplete="off">
{{#if sound}}
<h3>{{sound.name}}</h3>
{{else}}
<h3>{{_ "Custom_Sound_Add"}}</h3>
{{/if}}
<div class="input-line">
<label for="name">{{_ "Name"}}</label>
<input type="text" id="name" autocomplete="off" value="{{sound.name}}">
</div>
<div class="input-line">
<label for="image">{{_ "Sound_File_mp3"}}</label>
<input id="image" type="file" accept="audio/mp3,audio/mpeg,audio/x-mpeg,audio/mpeg3,audio/x-mpeg-3,.mp3"/>
</div>
<nav>
<button class='button button-block cancel' type="button"><span>{{_ "Cancel"}}</span></button>
<button class='button button-block primary save'><span>{{_ "Save"}}</span></button>
</nav>
</form>
</div>
{{/requiresPermission}}
</template>

@ -1,155 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import s from 'underscore.string';
import { t, handleError } from '../../../utils';
Template.soundEdit.helpers({
sound() {
return Template.instance().sound;
},
name() {
return this.name || this._id;
},
});
Template.soundEdit.events({
'click .cancel'(e, t) {
e.stopPropagation();
e.preventDefault();
delete Template.instance().soundFile;
t.cancel(t.find('form'));
},
'submit form'(e, t) {
e.stopPropagation();
e.preventDefault();
t.save(e.currentTarget);
},
'change input[type=file]'(ev) {
const e = ev.originalEvent != null ? ev.originalEvent : ev;
let { files } = e.target;
if (e.target.files == null || files.length === 0) {
if (e.dataTransfer.files != null) {
files = e.dataTransfer.files;
} else {
files = [];
}
}
// using let x of y here seems to have incompatibility with some phones
for (const file in files) {
if (files.hasOwnProperty(file)) {
Template.instance().soundFile = files[file];
}
}
},
});
Template.soundEdit.onCreated(function() {
if (this.data != null) {
this.sound = this.data.sound;
} else {
this.sound = undefined;
this.data.tabBar.showGroup('custom-sounds');
}
this.onSuccess = Template.currentData().onSuccess;
this.cancel = (form, name) => {
form.reset();
this.data.tabBar.close();
if (this.sound) {
this.data.back(name);
}
};
this.getSoundData = () => {
const soundData = {};
if (this.sound != null) {
soundData._id = this.sound._id;
soundData.previousName = this.sound.name;
soundData.extension = this.sound.extension;
soundData.previousExtension = this.sound.extension;
}
soundData.name = s.trim(this.$('#name').val());
soundData.newFile = false;
return soundData;
};
this.validate = () => {
const soundData = this.getSoundData();
const errors = [];
if (!soundData.name) {
errors.push('Name');
}
if (!soundData._id) {
if (!this.soundFile) {
errors.push('Sound_File_mp3');
}
}
for (const error of errors) {
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) }));
}
if (this.soundFile) {
if (!/audio\/mp3/.test(this.soundFile.type) && !/audio\/mpeg/.test(this.soundFile.type) && !/audio\/x-mpeg/.test(this.soundFile.type)) {
errors.push('FileType');
toastr.error(TAPi18n.__('error-invalid-file-type'));
}
}
return errors.length === 0;
};
this.save = (form) => {
if (this.validate()) {
const soundData = this.getSoundData();
if (this.soundFile) {
soundData.newFile = true;
soundData.extension = this.soundFile.name.split('.').pop();
soundData.type = this.soundFile.type;
}
Meteor.call('insertOrUpdateSound', soundData, (error, result) => {
if (result) {
soundData._id = result;
soundData.random = Math.round(Math.random() * 1000);
if (this.soundFile) {
toastr.info(TAPi18n.__('Uploading_file'));
const reader = new FileReader();
reader.readAsBinaryString(this.soundFile);
reader.onloadend = () => {
Meteor.call('uploadCustomSound', reader.result, this.soundFile.type, soundData, (uploadError/* , data*/) => {
if (uploadError != null) {
handleError(uploadError);
console.log(uploadError);
}
},
);
delete this.soundFile;
toastr.success(TAPi18n.__('File_uploaded'));
};
}
toastr.success(t('Custom_Sound_Saved_Successfully'));
this.onSuccess();
this.cancel(form, soundData.name);
}
if (error) {
handleError(error);
}
});
}
};
});

@ -1,19 +0,0 @@
<template name="soundInfo">
{{#if editingSound}}
{{> soundEdit (soundToEdit)}}
{{else}}
{{#with sound}}
<div class="about clearfix">
<div class="info">
<h3 title="{{name}}">{{name}}</h3>
</div>
</div>
{{/with}}
<nav>
{{#if hasPermission 'manage-sounds'}}
<button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button>
<button class='button button-block primary edit-sound'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button>
{{/if}}
</nav>
{{/if}}
</template>

@ -1,118 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { modal } from '../../../ui-utils';
import { t, handleError } from '../../../utils';
Template.soundInfo.helpers({
name() {
const sound = Template.instance().sound.get();
return sound.name;
},
sound() {
return Template.instance().sound.get();
},
editingSound() {
return Template.instance().editingSound.get();
},
soundToEdit() {
const instance = Template.instance();
return {
tabBar: instance.data.tabBar,
data: instance.data.data,
sound: instance.sound.get(),
onSuccess: instance.onSuccess,
back(name) {
instance.editingSound.set();
if (name != null) {
const sound = instance.sound.get();
if (sound.name != null && sound.name !== name) {
return instance.loadedName.set(name);
}
}
},
};
},
});
Template.soundInfo.events({
'click .delete'(e, instance) {
e.stopPropagation();
e.preventDefault();
const sound = instance.sound.get();
if (sound != null) {
const { _id } = sound;
modal.open({
title: t('Are_you_sure'),
text: t('Custom_Sound_Delete_Warning'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, function() {
Meteor.call('deleteCustomSound', _id, (error/* , result*/) => {
if (error) {
handleError(error);
} else {
modal.open({
title: t('Deleted'),
text: t('Custom_Sound_Has_Been_Deleted'),
type: 'success',
timer: 2000,
showConfirmButton: false,
});
instance.onSuccess();
instance.data.tabBar.showGroup('custom-sounds');
instance.data.tabBar.close();
}
});
});
}
},
'click .edit-sound'(e, instance) {
e.stopPropagation();
e.preventDefault();
instance.editingSound.set(instance.sound.get()._id);
},
});
Template.soundInfo.onCreated(function() {
this.sound = new ReactiveVar();
this.editingSound = new ReactiveVar();
this.loadedName = new ReactiveVar();
this.onSuccess = Template.currentData().onSuccess;
this.autorun(() => {
const data = Template.currentData();
if (data && data.clear != null) {
this.clear = data.clear;
}
});
this.autorun(() => {
const data = Template.currentData().sound;
const sound = this.sound.get();
if (sound && sound.name != null) {
this.loadedName.set(sound.name);
} else if (data.name != null) {
this.loadedName.set(data.name);
}
});
this.autorun(() => {
const data = Template.currentData().sound;
this.sound.set(data);
});
});

@ -1,11 +0,0 @@
import { hasAtLeastOnePermission } from '../../../authorization';
import { registerAdminSidebarItem } from '../../../ui-admin/client';
registerAdminSidebarItem({
href: 'custom-sounds',
i18nLabel: 'Custom_Sounds',
icon: 'volume',
permissionGranted() {
return hasAtLeastOnePermission(['manage-sounds']);
},
});

@ -1,8 +0,0 @@
import './adminSoundEdit.html';
import './adminSoundInfo.html';
import './adminSounds.html';
import './adminSounds';
import './soundEdit.html';
import './soundEdit';
import './soundInfo.html';
import './soundInfo';

@ -1,7 +1,4 @@
import './notifications/deleteCustomSound';
import './notifications/updateCustomSound';
import './admin/route';
import './admin/startup';
import '../assets/stylesheets/customSoundsAdmin.css';
export { CustomSounds } from './lib/CustomSounds';

@ -4,6 +4,8 @@ import _ from 'underscore';
import { CachedCollectionManager } from '../../../ui-cached-collection';
const getCustomSoundId = (sound) => `custom-sound-${ sound }`;
class CustomSoundsClass {
constructor() {
this.list = new ReactiveVar({});
@ -19,7 +21,7 @@ class CustomSoundsClass {
if (!sound.src) {
sound.src = this.getURL(sound);
}
const audio = $('<audio />', { id: sound._id, preload: true }).append(
const audio = $('<audio />', { id: getCustomSoundId(sound._id), preload: true }).append(
$('<source />', { src: sound.src }),
);
const list = this.list.get();
@ -59,7 +61,7 @@ class CustomSoundsClass {
}
play = (sound, { volume = 1, loop = false } = {}) => {
const audio = document.querySelector(`audio#${ sound }`);
const audio = document.querySelector(`#${ getCustomSoundId(sound) }`);
if (!audio || !audio.play) {
return;
}

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import s from 'underscore.string';
import { check } from 'meteor/check';
import { hasPermission } from '../../../authorization';
import { CustomSounds } from '../../../models';
@ -32,6 +33,7 @@ Meteor.methods({
let matchingResults = [];
if (soundData._id) {
check(soundData._id, String);
matchingResults = CustomSounds.findByNameExceptId(soundData.name, soundData._id).fetch();
} else {
matchingResults = CustomSounds.findByName(soundData.name).fetch();

@ -1,132 +0,0 @@
.emojiAdminPreview {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
border-radius: 4px;
& .emojiAdminPreview-image {
position: relative;
display: block;
width: 100%;
min-width: 20px;
height: 100%;
min-height: 20px;
border-radius: 4px;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
}
.emoji-view {
z-index: 15;
overflow-x: hidden;
overflow-y: auto;
& .thumb {
width: 100%;
height: 350px;
padding: 20px;
}
& nav {
padding: 0 20px;
}
& .info {
padding: 0 20px;
white-space: normal;
& h3 {
overflow: hidden;
width: 100%;
margin: 8px 0;
user-select: text;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 24px;
line-height: 27px;
& i::after {
display: inline-block;
width: 8px;
height: 8px;
content: " ";
vertical-align: middle;
border-radius: 4px;
}
}
& p {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
font-size: 12px;
font-weight: 300;
line-height: 18px;
}
}
& .edit-form {
padding: 20px 20px 0;
white-space: normal;
& h3 {
margin-bottom: 8px;
font-size: 24px;
line-height: 22px;
}
& p {
font-size: 12px;
font-weight: 300;
line-height: 18px;
}
& > .input-line {
margin-top: 20px;
}
& nav {
padding: 0;
&.buttons {
margin-top: 2em;
}
}
& .form-divisor {
height: 9px;
margin: 2em 0;
text-align: center;
& > span {
padding: 0 1em;
}
}
}
& .room-info-content > div {
margin: 0 0 20px;
}
}

@ -1,76 +0,0 @@
<template name="adminEmoji">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Custom_emoji"}}
<div class="content">
{{#unless hasPermission 'manage-emoji'}}
<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 isLoading}}
{{> loading }}
{{else}}
{{> icon block="rc-input__icon-svg" icon="magnifier" }}
{{/if}}
</div>
<input id="emoji-filter" type="text" class="rc-input__element"
placeholder="{{_ "Search"}}" autofocus dir="auto">
</div>
</form>
<div class="results">
{{{_ "Showing_results" customemoji.length}}}
</div>
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr>
<th width="50%">
<div class="table-fake-th">{{_ "Name"}}</div>
</th>
<th width="50%">
<div class="table-fake-th">{{_ "Aliases"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each customemoji}}
<tr>
<td width="50%">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">
{{name}}
</span>
</div>
</div>
</td>
<td width="50%">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">
{{aliases}}
</span>
</div>
</div>
</td>
</tr>
{{else}} {{# with searchText}}
<tr class="table-no-click">
<td>{{_ "No_results_found_for"}} {{.}}</td>
</tr>
{{/with}} {{/each}} {{#if isLoading}}
<tr class="table-no-click">
<td class="table-loading-td">{{> loading}}</td>
</tr>
{{/if}}
</tbody>
{{/table}}
{{/unless}}
</div>
</section>
{{#with flexData}}
{{> flexTabBar}}
{{/with}}
</div>
</template>

@ -1,141 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import _ from 'underscore';
import { RocketChatTabBar, SideNav, TabBar } from '../../../ui-utils';
import { APIClient } from '../../../utils/client';
const LIST_SIZE = 50;
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500;
Template.adminEmoji.helpers({
searchText() {
const instance = Template.instance();
return instance.filter && instance.filter.get();
},
customemoji() {
return Template.instance().emojis.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
flexData() {
return {
tabBar: Template.instance().tabBar,
data: Template.instance().tabBarData.get(),
};
},
onTableScroll() {
const instance = Template.instance();
return function(currentTarget) {
if ((currentTarget.offsetHeight + currentTarget.scrollTop) < (currentTarget.scrollHeight - 100)) {
return;
}
const emojis = instance.emojis.get();
if (instance.total.get() > emojis.length) {
instance.offset.set(instance.offset.get() + LIST_SIZE);
}
};
},
onTableItemClick() {
const instance = Template.instance();
return function({ _id }) {
instance.tabBarData.set({
emoji: instance.emojis.get().find((emoji) => emoji._id === _id),
onSuccess: instance.onSuccessCallback,
});
instance.tabBar.open('admin-emoji-info');
};
},
});
Template.adminEmoji.onCreated(async function() {
const instance = this;
this.emojis = new ReactiveVar([]);
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.filter = new ReactiveVar('');
this.query = new ReactiveVar({});
this.isLoading = new ReactiveVar(false);
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
this.tabBarData = new ReactiveVar();
TabBar.addButton({
groups: ['emoji-custom'],
id: 'add-emoji',
i18nTitle: 'Custom_Emoji_Add',
icon: 'plus',
template: 'adminEmojiEdit',
order: 1,
});
TabBar.addButton({
groups: ['emoji-custom'],
id: 'admin-emoji-info',
i18nTitle: 'Custom_Emoji_Info',
icon: 'customize',
template: 'adminEmojiInfo',
order: 2,
});
this.onSuccessCallback = () => {
this.offset.set(0);
return this.loadEmojis(this.query.get(), this.offset.get());
};
this.tabBarData.set({
onSuccess: instance.onSuccessCallback,
});
this.loadEmojis = _.debounce(async (query, offset) => {
this.isLoading.set(true);
const { emojis, total } = await APIClient.v1.get(`emoji-custom.all?count=${ LIST_SIZE }&offset=${ offset }&query=${ JSON.stringify(query) }`);
this.total.set(total);
if (offset === 0) {
this.emojis.set(emojis);
} else {
this.emojis.set(this.emojis.get().concat(emojis));
}
this.isLoading.set(false);
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS);
this.autorun(() => {
this.filter.get();
this.offset.set(0);
});
this.autorun(() => {
const filter = this.filter.get() && this.filter.get().trim();
const offset = this.offset.get();
if (filter) {
const regex = { $regex: filter, $options: 'i' };
return this.loadEmojis({ $or: [{ name: regex }, { aliases: regex }] }, offset);
}
return this.loadEmojis({}, offset);
});
});
Template.adminEmoji.onRendered(() =>
Tracker.afterFlush(function() {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}),
);
Template.adminEmoji.events({
'keydown #emoji-filter'(e) {
// stop enter key
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
}
},
'keyup #emoji-filter'(e, t) {
e.stopPropagation();
e.preventDefault();
t.filter.set(e.currentTarget.value);
},
});

@ -1,7 +0,0 @@
<template name="adminEmojiEdit">
<div class="content">
<div class="emoji-view">
{{> emojiEdit .}}
</div>
</div>
</template>

@ -1,7 +0,0 @@
<template name="adminEmojiInfo">
<div class="content">
<div class="emoji-view">
{{> emojiInfo .}}
</div>
</div>
</template>

@ -1,31 +0,0 @@
<template name="emojiEdit">
{{#unless hasPermission 'manage-emoji'}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<div class="about clearfix">
<form class="edit-form" autocomplete="off">
{{#if emoji}}
<h3>{{emoji.name}}</h3>
{{else}}
<h3>{{_ "Custom_Emoji_Add"}}</h3>
{{/if}}
<div class="input-line">
<label for="name">{{_ "Name"}}</label>
<input type="text" id="name" autocomplete="off" value="{{emoji.name}}" class="content-background-color">
</div>
<div class="input-line">
<label for="aliases">{{_ "Aliases"}}</label>
<input type="text" id="aliases" autocomplete="off" value="{{emoji.aliases}}" class="content-background-color">
</div>
<div class="input-line">
<label for="image">{{_ "Image"}}</label>
<input id="image" type="file" />
</div>
<nav>
<button class='button button-block cancel' type="button"><span>{{_ "Cancel"}}</span></button>
<button class='button button-block primary save'><span>{{_ "Save"}}</span></button>
</nav>
</form>
</div>
{{/unless}}
</template>

@ -1,158 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import s from 'underscore.string';
import { t, handleError } from '../../../utils';
Template.emojiEdit.helpers({
emoji() {
return Template.instance().emoji;
},
name() {
return this.name || this._id;
},
});
Template.emojiEdit.events({
'click .cancel'(e, t) {
e.stopPropagation();
e.preventDefault();
delete Template.instance().emojiFile;
t.cancel(t.find('form'));
},
'submit form'(e, t) {
e.stopPropagation();
e.preventDefault();
t.save(e.currentTarget);
},
'change input[type=file]'(ev) {
const e = ev.originalEvent != null ? ev.originalEvent : ev;
let { files } = e.target;
if (files == null || files.length === 0) {
if (e.dataTransfer != null && e.dataTransfer.files != null) {
files = e.dataTransfer.files;
} else {
files = [];
}
}
// using let x of y here seems to have incompatibility with some phones
for (const file in files) {
if (files.hasOwnProperty(file)) {
Template.instance().emojiFile = files[file];
}
}
},
});
Template.emojiEdit.onCreated(function() {
if (this.data != null) {
this.emoji = this.data.emoji;
} else {
this.emoji = undefined;
}
this.tabBar = Template.currentData().tabBar;
this.onSuccess = Template.currentData().onSuccess;
this.cancel = (form, name) => {
form.reset();
this.tabBar.close();
if (this.emoji) {
this.data.back(name);
}
};
this.getEmojiData = () => {
const emojiData = {};
if (this.emoji != null) {
emojiData._id = this.emoji._id;
emojiData.previousName = this.emoji.name;
emojiData.extension = this.emoji.extension;
emojiData.previousExtension = this.emoji.extension;
}
emojiData.name = s.trim(this.$('#name').val());
emojiData.aliases = s.trim(this.$('#aliases').val());
emojiData.newFile = false;
return emojiData;
};
this.validate = () => {
const emojiData = this.getEmojiData();
const errors = [];
if (!emojiData.name) {
errors.push('Name');
}
if (!emojiData._id) {
if (!this.emojiFile) {
errors.push('Image');
}
}
for (const error of errors) {
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) }));
}
if (this.emojiFile) {
if (!/image\/.+/.test(this.emojiFile.type)) {
errors.push('FileType');
toastr.error(TAPi18n.__('error-invalid-file-type'));
}
}
return errors.length === 0;
};
this.save = (form) => {
if (this.validate()) {
const emojiData = this.getEmojiData();
if (this.emojiFile) {
emojiData.newFile = true;
emojiData.extension = this.emojiFile.name.split('.').pop();
}
Meteor.call('insertOrUpdateEmoji', emojiData, (error, result) => {
if (result) {
if (this.emojiFile) {
toastr.info(TAPi18n.__('Uploading_file'));
const reader = new FileReader();
reader.readAsBinaryString(this.emojiFile);
reader.onloadend = () => {
Meteor.call('uploadEmojiCustom', reader.result, this.emojiFile.type, emojiData, (uploadError/* , data*/) => {
if (uploadError != null) {
handleError(uploadError);
console.log(uploadError);
}
},
);
delete this.emojiFile;
toastr.success(TAPi18n.__('File_uploaded'));
};
}
if (emojiData._id) {
toastr.success(t('Custom_Emoji_Updated_Successfully'));
} else {
toastr.success(t('Custom_Emoji_Added_Successfully'));
}
this.onSuccess();
this.cancel(form, emojiData.name);
}
if (error) {
handleError(error);
}
});
}
};
});

@ -1,23 +0,0 @@
<template name="emojiInfo">
{{#if editingEmoji}}
{{> emojiEdit (emojiToEdit)}}
{{else}}
{{#with emoji}}
<div class="about clearfix">
<div class="thumb">
{{> emojiPreview name=name extension=extension}}
</div>
<div class="info">
<h3 title="{{name}}">{{name}}</h3>
<h3 title="{{aliases}}">{{aliases}}</h3>
</div>
</div>
{{/with}}
<nav>
{{#if hasPermission 'manage-emoji'}}
<button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button>
<button class='button button-block primary edit-emoji'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button>
{{/if}}
</nav>
{{/if}}
</template>

@ -1,126 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { handleError, t } from '../../../utils';
import { modal } from '../../../ui-utils';
Template.emojiInfo.helpers({
name() {
const emoji = Template.instance().emoji.get();
return emoji.name;
},
aliases() {
const emoji = Template.instance().emoji.get();
return emoji.aliases;
},
emoji() {
return Template.instance().emoji.get();
},
editingEmoji() {
return Template.instance().editingEmoji.get();
},
emojiToEdit() {
const instance = Template.instance();
return {
tabBar: this.tabBar,
emoji: instance.emoji.get(),
onSuccess: instance.onSuccess,
back(name) {
instance.editingEmoji.set();
if (name != null) {
const emoji = instance.emoji.get();
if (emoji != null && emoji.name != null && emoji.name !== name) {
return instance.loadedName.set(name);
}
}
},
};
},
});
Template.emojiInfo.events({
'click .thumb'(e) {
$(e.currentTarget).toggleClass('bigger');
},
'click .delete'(e, instance) {
e.stopPropagation();
e.preventDefault();
const emoji = instance.emoji.get();
if (emoji != null) {
const { _id } = emoji;
modal.open({
title: t('Are_you_sure'),
text: t('Custom_Emoji_Delete_Warning'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, function() {
Meteor.call('deleteEmojiCustom', _id, (error/* , result*/) => {
if (error) {
return handleError(error);
}
modal.open({
title: t('Deleted'),
text: t('Custom_Emoji_Has_Been_Deleted'),
type: 'success',
timer: 2000,
showConfirmButton: false,
});
instance.onSuccess();
instance.tabBar.close();
});
});
}
},
'click .edit-emoji'(e, instance) {
e.stopPropagation();
e.preventDefault();
instance.editingEmoji.set(instance.emoji.get()._id);
},
});
Template.emojiInfo.onCreated(function() {
this.emoji = new ReactiveVar();
this.onSuccess = Template.currentData().onSuccess;
this.editingEmoji = 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().emoji;
const emoji = this.emoji.get();
if (emoji != null && emoji.name != null) {
this.loadedName.set(emoji.name);
} else if (data != null && data.name != null) {
this.loadedName.set(data.name);
}
});
this.autorun(() => {
const data = Template.currentData().emoji;
this.emoji.set(data);
});
});

@ -1,5 +0,0 @@
<template name="emojiPreview">
<div class="emojiAdminPreview">
<div class="emojiAdminPreview-image" data-emoji="{{this.name}}" style="background-image:url('{{emojiUrlFromName this.name this.extension}}');"></div>
</div>
</template>

@ -1,11 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../ui-admin/client';
registerAdminRoute('/emoji-custom', {
name: 'emoji-custom',
async action(/* params*/) {
await import('./views');
BlazeLayout.render('main', { center: 'adminEmoji' });
},
});

@ -1,5 +1,5 @@
import { hasPermission } from '../../../authorization';
import { registerAdminSidebarItem } from '../../../ui-admin/client';
import { registerAdminSidebarItem } from '../../../../client/admin';
registerAdminSidebarItem({
href: 'emoji-custom',

@ -1,9 +0,0 @@
import './adminEmoji.html';
import './adminEmoji';
import './adminEmojiEdit.html';
import './adminEmojiInfo.html';
import './emojiEdit.html';
import './emojiEdit';
import './emojiInfo.html';
import './emojiInfo';
import './emojiPreview.html';

@ -2,5 +2,3 @@ import './lib/emojiCustom';
import './notifications/deleteEmojiCustom';
import './notifications/updateEmojiCustom';
import './admin/startup';
import './admin/route';
import '../assets/stylesheets/emojiCustomAdmin.css';

@ -5,7 +5,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { hasRole } from '../../../authorization';
import { registerAdminRoute, registerAdminSidebarItem } from '../../../ui-admin/client';
import { registerAdminRoute, registerAdminSidebarItem } from '../../../../client/admin';
import './dashboard.html';
import './dashboard.css';

@ -1,35 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { CachedCollectionManager } from '../../ui-cached-collection';
class ImporterWebsocketReceiverDef {
constructor() {
this.streamer = new Meteor.Streamer('importers');
this.callbacks = [];
CachedCollectionManager.onLogin(() => {
this.streamer.on('progress', this.progressUpdated.bind(this));
});
}
progressUpdated(progress) {
this.callbacks.forEach((c) => c(progress));
}
registerCallback(callback) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function.');
}
this.callbacks.push(callback);
}
unregisterCallback(callback) {
const i = this.callbacks.indexOf(callback);
if (i >= 0) {
this.callbacks.splice(i, 1);
}
}
}
export const ImporterWebsocketReceiver = new ImporterWebsocketReceiverDef();

@ -1,115 +0,0 @@
<template name="importOperationSummary">
<span style="font-weight: 600;">{{_ "Import_Type"}}:</span>
<span>{{_ type}} [{{ importerKey}}]</span>
<br/>
<span style="font-weight: 600;">{{_ "Last_Updated"}}:</span>
<span>{{ lastUpdated}}</span>
<br/>
{{#if valid}}
<span style="font-weight: 600;">{{_ "Status"}}:</span>
{{else}}
<span style="font-weight: 600;">{{_ "Last_Status"}}:</span>
{{/if}}
<span>{{ status}}</span>
<br/>
{{#if file}}
<span style="font-weight: 600;">{{_ "File"}}:</span>
<span>{{ fileName}}</span>
<br/>
{{/if}}
{{#if hasCounters}}
<span style="font-weight: 600;">{{_ "Counters"}}:</span>
<br/>
<span>{{_ "Users"}}:</span>
<span>{{ userCount}}</span>
<br/>
<span>{{_ "Channels"}}:</span>
<span>{{ channelCount}}</span>
<br/>
<span>{{_ "Messages"}}:</span>
<span>{{ messageCount}}</span>
<br/>
<span>{{_ "Total"}}:</span>
<span>{{ totalCount}}</span>
<br/>
<br/>
{{/if}}
{{#if hasErrors}}
<h2>{{_ "Errors_and_Warnings"}}:</h2>
<br/>
{{#if fileData.users}}
{{#each fileData.users}}
{{#if is_email_taken}}
<span style="color: red;">{{_ "Email_already_exists"}}:</span>
<span>{{ email}}</span>
<br/>
{{/if}}
{{#if error}}
<span>{{ email}}:</span>
<span style="color: red;">{{ formatedError}}</span>
<br/>
{{/if}}
{{/each}}
{{/if}}
{{#if errors}}
<br/>
<br/>
<h3>{{_ "Not_Imported_Messages_Title"}}</h3>
<br/>
{{#each errors}}
<br/>
{{#if msg}}
{{#if msg.id}}
<span style="font-weight: 600;">{{_ "Message_Id"}}:</span>
<span>{{ msg.id}}</span>
<br/>
{{/if}}
{{#if msg.userId}}
<span style="font-weight: 600;">{{_ "Message_UserId"}}:</span>
<span>{{ msg.userId}}</span>
<br/>
{{/if}}
{{#if msg.ts}}
<span style="font-weight: 600;">{{_ "Message_Time"}}:</span>
<span>{{ messageTime}}</span>
<br/>
{{/if}}
{{/if}}
{{#if error}}
{{#if error.error}}
<span style="font-weight: 500;">{{_ "Error"}}:</span>
<span style="color: red;">{{ error.error}}:</span>
<br/>
{{/if}}
{{#if error.message}}
<span style="font-weight: 500;">{{_ "Message"}}:</span>
<span style="color: red;">{{ error.message}}:</span>
<br/>
{{/if}}
{{#if error.details}}
{{#if error.details.method}}
<span style="font-weight: 500;">{{_ "Method"}}:</span>
<span style="color: red;">{{ error.details.method}}:</span>
<br/>
{{/if}}
{{/if}}
{{/if}}
{{/each}}
{{/if}}
<br/>
{{/if}}
</template>

@ -1,41 +0,0 @@
import toastr from 'toastr';
import { t } from '../../../utils';
function showToastrForErrorType(errorType, defaultErrorString) {
let errorCode;
if (typeof errorType === 'string') {
errorCode = errorType;
} else if (typeof errorType === 'object') {
if (errorType.error && typeof errorType.error === 'string') {
errorCode = errorType.error;
}
}
if (errorCode) {
const errorTranslation = t(errorCode);
if (errorTranslation !== errorCode) {
toastr.error(errorTranslation);
return;
}
}
toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File'));
}
function showException(error, defaultErrorString) {
console.error(error);
if (error && error.xhr && error.xhr.responseJSON) {
console.log(error.xhr.responseJSON);
if (error.xhr.responseJSON.errorType) {
showToastrForErrorType(error.xhr.responseJSON.errorType, defaultErrorString);
return;
}
}
toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File'));
}
export const showImporterException = showException;

@ -1,6 +1,3 @@
import './routes';
export { Importers } from '../lib/Importers';
export { ImporterInfo } from '../lib/ImporterInfo';
export { ProgressStep } from '../lib/ImporterProgressStep';
export { ImporterWebsocketReceiver } from './ImporterWebsocketReceiver';

@ -1,25 +0,0 @@
import { registerAdminRoute } from '../../ui-admin/client';
registerAdminRoute('/import', {
name: 'admin-import',
lazyRouteComponent: () => import('./components/ImportRoute'),
props: { page: 'history' },
});
registerAdminRoute('/import/new/:importerKey?', {
name: 'admin-import-new',
lazyRouteComponent: () => import('./components/ImportRoute'),
props: { page: 'new' },
});
registerAdminRoute('/import/prepare', {
name: 'admin-import-prepare',
lazyRouteComponent: () => import('./components/ImportRoute'),
props: { page: 'prepare' },
});
registerAdminRoute('/import/progress', {
name: 'admin-import-progress',
lazyRouteComponent: () => import('./components/ImportRoute'),
props: { page: 'progress' },
});

@ -1,6 +1,6 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../ui-admin/client';
import { registerAdminRoute } from '../../../client/admin';
import { t } from '../../utils';
const dynamic = () => import('./views');

@ -1,5 +1,5 @@
import { hasAtLeastOnePermission } from '../../authorization';
import { registerAdminSidebarItem } from '../../ui-admin/client';
import { registerAdminSidebarItem } from '../../../client/admin';
registerAdminSidebarItem({
href: 'admin-integrations',

@ -1,80 +0,0 @@
<template name="adminInvites">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Invites"}}
<div class="content">
{{#unless hasPermission 'create-invite-links'}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<div class="results">
{{{_ "Showing_results" invites.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="20%">
<div class="table-fake-th">{{_ "Token"}}</div>
</th>
<th class="content-background-color border-component-color" width="35%">
<div class="table-fake-th">{{_ "Created_at"}}</div>
</th>
<th class="content-background-color border-component-color" width="20%">
<div class="table-fake-th">{{_ "Expiration"}}</div>
</th>
<th class="content-background-color border-component-color" width="10%">
<div class="table-fake-th">{{_ "Uses"}}</div>
</th>
<th class="content-background-color border-component-color" width="15%">
<div class="table-fake-th">{{_ "Uses_left"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each invites}}
<tr class="invites-info row-link admin-table-row">
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{_id}}</span></div>
</div>
</td>
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{formatDateAndTime createdAt}}</span></div>
</div>
</td>
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{daysToExpire}}</span></div>
</div>
</td>
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{uses}}</span></div>
</div>
</td>
<td class="border-component-color">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{maxUsesLeft}}</span></div>
<button class="rc-apps-section__app-menu-trigger js-remove" data-id="{{_id}}">
<svg class="rc-icon rc-icon--default-size rc-icon--default-size--menu" aria-hidden="true">
<use xlink:href="#icon-cross"></use>
</svg>
</button>
</div>
</td>
</tr>
{{/each}}
</tbody>
{{/table}}
{{/unless}}
</div>
</section>
</div>
</template>

@ -1,93 +0,0 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import moment from 'moment';
import toastr from 'toastr';
import { t, APIClient } from '../../../utils';
import { formatDateAndTime } from '../../../lib/client/lib/formatDate';
import { modal } from '../../../ui-utils/client';
import './adminInvites.html';
Template.adminInvites.helpers({
formatDateAndTime,
invites() {
return Template.instance().invites.get();
},
daysToExpire() {
const { expires, days } = this;
if (days > 0) {
if (expires < Date.now()) {
return t('Expired');
}
return moment(expires).fromNow(true);
}
return t('Never');
},
maxUsesLeft() {
const { maxUses, uses } = this;
if (maxUses > 0) {
if (uses >= maxUses) {
return 0;
}
return maxUses - uses;
}
return t('Unlimited');
},
});
Template.adminInvites.onCreated(async function() {
const instance = this;
this.invites = new ReactiveVar([]);
const result = await APIClient.v1.get('listInvites') || [];
const invites = result.map((data) => ({
...data,
createdAt: new Date(data.createdAt),
expires: data.expires ? new Date(data.expires) : '',
}));
instance.invites.set(invites);
});
Template.adminInvites.events({
async 'click .js-remove'(event, instance) {
event.stopPropagation();
modal.open({
text: t('Are_you_sure_you_want_to_delete_this_record'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes'),
cancelButtonText: t('No'),
closeOnConfirm: true,
html: false,
}, async (confirmed) => {
if (!confirmed) {
return;
}
const { currentTarget } = event;
const { id } = currentTarget.dataset;
try {
await APIClient.v1.delete(`removeInvite/${ id }`);
const invites = instance.invites.get() || [];
invites.splice(invites.findIndex((i) => i._id === id), 1);
instance.invites.set(invites);
} catch (e) {
toastr.error(t(e.error));
}
});
},
});

@ -1,11 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../ui-admin/client';
registerAdminRoute('/invites', {
name: 'invites',
async action(/* params */) {
await import('./adminInvites');
BlazeLayout.render('main', { center: 'adminInvites' });
},
});

@ -1,11 +0,0 @@
import { hasAtLeastOnePermission } from '../../../authorization';
import { registerAdminSidebarItem } from '../../../ui-admin/client';
registerAdminSidebarItem({
href: 'invites',
i18nLabel: 'Invites',
icon: 'user-plus',
permissionGranted() {
return hasAtLeastOnePermission(['create-invite-links']);
},
});

@ -1,2 +0,0 @@
import './admin/route';
import './admin/startup';

@ -73,7 +73,7 @@ export class DirectMessageRoomType extends RoomTypeConfig {
: Subscriptions.findOne({ rid: roomData._id });
if (subscription === undefined) {
return console.log('roomData', roomData);
return;
}
if (settings.get('UI_Use_Real_Name') && subscription.fname) {

@ -1,34 +0,0 @@
const foregroundColors = {
30: 'gray',
31: 'red',
32: 'lime',
33: 'yellow',
34: '#6B98FF',
35: '#FF00FF',
36: 'cyan',
37: 'white',
};
export const ansispan = (str: string) => {
str = str
.replace(/\s/g, '&nbsp;')
.replace(/(\\n|\n)/g, '<br>')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/(.\d{8}-\d\d:\d\d:\d\d\.\d\d\d\(?.{0,2}\)?)/, '<span class="terminal-time">$1</span>')
.replace(/\033\[1m/g, '<strong>')
.replace(/\033\[22m/g, '</strong>')
.replace(/\033\[3m/g, '<em>')
.replace(/\033\[23m/g, '</em>')
.replace(/\033\[m/g, '</span>')
.replace(/\033\[0m/g, '</span>')
.replace(/\033\[39m/g, '</span>');
return Object.entries(foregroundColors).reduce((str, [ansiCode, color]) => {
const span = `<span style="color: ${ color }">`;
return (
str
.replace(new RegExp(`\\033\\[${ ansiCode }m`, 'g'), span)
.replace(new RegExp(`\\033\\[0;${ ansiCode }m`, 'g'), span)
);
}, str);
};

@ -1,2 +1 @@
import './logger';
import './viewLogs';

@ -1,33 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { hasAllPermission } from '../../authorization';
import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/client';
import { t } from '../../utils';
export const stdout = new Mongo.Collection(null);
Meteor.startup(function() {
registerAdminSidebarItem({
href: 'admin-view-logs',
i18nLabel: 'View_Logs',
icon: 'post',
permissionGranted() {
return hasAllPermission('view-logs');
},
});
});
registerAdminRoute('/view-logs', {
name: 'admin-view-logs',
async action() {
await import('./views/viewLogs');
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('View_Logs'),
pageTemplate: 'viewLogs',
noScroll: true,
});
},
});

@ -1,29 +0,0 @@
.view-logs {
&__terminal {
overflow-y: scroll;
flex: 1;
margin: 0;
margin-bottom: 0 !important;
padding: 8px 10px !important;
color: var(--color-white);
border: none !important;
background-color: #444444 !important;
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-weight: 500;
&-line {
word-break: break-all;
}
&-time {
color: #7f7f7f;
}
.rtl & {
direction: ltr;
}
}
}

@ -1,17 +0,0 @@
<template name="viewLogs">
{{#if hasPermission}}
<div class="section view-logs__terminal js-terminal">
{{#each logs}}
<div class="view-logs__terminal-line">
{{{ansispan string}}}
</div>
{{/each}}
</div>
<div class="view-logs__new-logs js-new-logs not color-primary-action-color">
<i class="icon-down-big"></i>
<span>{{_ "New_logs"}}</span>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</template>

@ -1,121 +0,0 @@
import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { ansispan } from '../ansispan';
import { stdout } from '../viewLogs';
import { hasAllPermission } from '../../../authorization';
import { SideNav } from '../../../ui-utils/client';
import './viewLogs.html';
import './viewLogs.css';
import { APIClient } from '../../../utils/client';
const stdoutStreamer = new Meteor.Streamer('stdout');
Template.viewLogs.onCreated(async function() {
const { queue } = await APIClient.v1.get('stdout.queue');
(queue || []).forEach((item) => stdout.insert(item));
stdoutStreamer.on('stdout', (item) => stdout.insert(item));
this.atBottom = true;
});
Template.viewLogs.onDestroyed(() => {
stdout.remove({});
stdoutStreamer.removeListener('stdout');
});
Template.viewLogs.helpers({
hasPermission() {
return hasAllPermission('view-logs');
},
logs() {
return stdout.find({}, { sort: { ts: 1 } });
},
ansispan,
});
Template.viewLogs.events({
'click .new-logs'(event, instance) {
instance.atBottom = true;
instance.sendToBottomIfNecessary();
},
});
Template.viewLogs.onRendered(function() {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
const wrapper = this.find('.js-terminal');
const newLogs = this.find('.js-new-logs');
this.isAtBottom = (scrollThreshold) => {
if (scrollThreshold == null) {
scrollThreshold = 0;
}
if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) {
newLogs.className = 'new-logs not';
return true;
}
return false;
};
this.sendToBottom = () => {
wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight;
newLogs.className = 'new-logs not';
};
this.checkIfScrollIsAtBottom = () => {
this.atBottom = this.isAtBottom(100);
};
this.sendToBottomIfNecessary = () => {
if (this.atBottom === true && this.isAtBottom() !== true) {
this.sendToBottom();
} else if (this.atBottom === false) {
newLogs.className = 'new-logs';
}
};
this.sendToBottomIfNecessaryDebounced = _.debounce(this.sendToBottomIfNecessary, 10);
this.sendToBottomIfNecessary();
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(() => this.sendToBottomIfNecessaryDebounced());
});
observer.observe(wrapper, { childList: true });
} else {
wrapper.addEventListener('DOMSubtreeModified', () => this.sendToBottomIfNecessaryDebounced());
}
this.onWindowResize = () => {
Meteor.defer(() => this.sendToBottomIfNecessaryDebounced());
};
window.addEventListener('resize', this.onWindowResize);
wrapper.addEventListener('mousewheel', () => {
this.atBottom = false;
Meteor.defer(() => this.checkIfScrollIsAtBottom());
});
wrapper.addEventListener('wheel', () => {
this.atBottom = false;
Meteor.defer(() => this.checkIfScrollIsAtBottom());
});
wrapper.addEventListener('touchstart', () => {
this.atBottom = false;
});
wrapper.addEventListener('touchend', () => {
Meteor.defer(() => this.checkIfScrollIsAtBottom());
});
wrapper.addEventListener('scroll', () => {
this.atBottom = false;
Meteor.defer(() => this.checkIfScrollIsAtBottom());
});
});

@ -1,5 +1,5 @@
import { hasAllPermission } from '../../authorization';
import { registerAdminSidebarItem } from '../../ui-admin/client';
import { registerAdminSidebarItem } from '../../../client/admin';
registerAdminSidebarItem({
href: 'admin-mailer',

@ -1,6 +1,6 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../ui-admin/client';
import { registerAdminRoute } from '../../../../client/admin';
import { t } from '../../../utils';
registerAdminRoute('/oauth-apps', {

@ -1,5 +1,5 @@
import { hasAllPermission } from '../../../authorization';
import { registerAdminSidebarItem } from '../../../ui-admin/client';
import { registerAdminSidebarItem } from '../../../../client/admin';
registerAdminSidebarItem({
href: 'admin-oauth-apps',

@ -4339,29 +4339,6 @@ rc-old select,
}
}
.rc-old .statistics-table {
width: 100%;
margin-bottom: 30px;
& th,
& td {
padding: 6px 8px;
text-align: left;
word-break: break-word;
}
& th {
width: 30%;
text-align: right;
}
& td {
width: 70%;
}
}
.rc-old .rocket-team {
display: block;
@ -4704,30 +4681,6 @@ rc-old select,
background-color: #f8f8f8;
}
.rc-old .new-logs {
position: absolute;
bottom: 8px;
left: 50%;
width: 130px;
height: 30px;
margin: 0 -65px;
cursor: pointer;
transition: transform 0.3s ease-out;
transform: translateY(0);
text-align: center;
border-radius: 20px;
font-size: 0.8em;
line-height: 30px;
&.not {
transform: translateY(150%);
}
}
.rc-old .powered-by {
margin-top: 1em;
}

@ -466,13 +466,6 @@
}
}
& .statistics-table {
& th,
& td {
text-align: right;
}
}
& .code-mirror-box {
direction: ltr;

@ -1,17 +0,0 @@
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../../client/contexts/TranslationContext';
import { Page } from '../../../../client/components/basic/Page';
function NotAuthorizedPage() {
const t = useTranslation();
return <Page>
<Page.Content>
<Box is='p' textColor='default' textStyle='p1'>{t('You_are_not_authorized_to_view_this_page')}</Box>
</Page.Content>
</Page>;
}
export default NotAuthorizedPage;

@ -1,22 +0,0 @@
import { Subtitle } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import { DescriptionList } from './DescriptionList';
import { formatDate } from './formatters';
export function BuildEnvironmentSection({ info }) {
const t = useTranslation();
const build = info && (info.compile || info.build);
return <>
<Subtitle data-qa='build-env-title'>{t('Build_Environment')}</Subtitle>
<DescriptionList data-qa='build-env-list'>
<DescriptionList.Entry label={t('OS_Platform')}>{build.platform}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Arch')}>{build.arch}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Release')}>{build.osRelease}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Node_version')}>{build.nodeVersion}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Date')}>{formatDate(build.date)}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -1,22 +0,0 @@
import { Subtitle } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import { DescriptionList } from './DescriptionList';
export function CommitSection({ info }) {
const t = useTranslation();
const { commit = {} } = info;
return <>
<Subtitle data-qa='commit-title'>{t('Commit')}</Subtitle>
<DescriptionList data-qa='commit-list'>
<DescriptionList.Entry label={t('Hash')}>{commit.hash}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Date')}>{commit.date}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Branch')}>{commit.branch}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Tag')}>{commit.tag}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Author')}>{commit.author}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Subject')}>{commit.subject}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -1,16 +0,0 @@
import React from 'react';
export const DescriptionList = ({ children, ...props }) =>
<table className='statistics-table secondary-background-color' {...props}>
<tbody>
{children}
</tbody>
</table>;
const Entry = ({ children, label, ...props }) =>
<tr className='admin-table-row' {...props}>
<th className='content-background-color border-component-color'>{label}</th>
<td className='border-component-color'>{children}</td>
</tr>;
DescriptionList.Entry = Entry;

@ -1,29 +0,0 @@
import { Skeleton, Subtitle } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import { formatDate, formatHumanReadableTime } from './formatters';
import { DescriptionList } from './DescriptionList';
export function RocketChatSection({ info, statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const t = useTranslation();
const appsEngineVersion = info && info.marketplaceApiVersion;
return <>
<Subtitle data-qa='rocket-chat-title'>{t('Rocket.Chat')}</Subtitle>
<DescriptionList data-qa='rocket-chat-list'>
<DescriptionList.Entry label={t('Version')}>{s(() => statistics.version)}</DescriptionList.Entry>
{appsEngineVersion && <DescriptionList.Entry label={t('Apps_Engine_Version')}>{appsEngineVersion}</DescriptionList.Entry>}
<DescriptionList.Entry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('PID')}>{s(() => statistics.process.pid)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -1,31 +0,0 @@
import { Skeleton, Subtitle } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import { useFormatMemorySize } from '../../../../ui/client/views/app/components/hooks';
import { DescriptionList } from './DescriptionList';
import { formatHumanReadableTime, formatCPULoad } from './formatters';
export function RuntimeEnvironmentSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const t = useTranslation();
const formatMemorySize = useFormatMemorySize();
return <>
<Subtitle data-qa='runtime-env-title'>{t('Runtime_Environment')}</Subtitle>
<DescriptionList data-qa='runtime-env-list'>
<DescriptionList.Entry label={t('OS_Type')}>{s(() => statistics.os.type)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Platform')}>{s(() => statistics.os.platform)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Arch')}>{s(() => statistics.os.arch)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Release')}>{s(() => statistics.os.release)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -1,51 +0,0 @@
import { Subtitle, Skeleton } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import { useFormatMemorySize } from '../../../../ui/client/views/app/components/hooks';
import { DescriptionList } from './DescriptionList';
export function UsageSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const formatMemorySize = useFormatMemorySize();
const t = useTranslation();
return <>
<Subtitle data-qa='usage-title'>{t('Usage')}</Subtitle>
<DescriptionList data-qa='usage-list'>
<DescriptionList.Entry label={t('Stats_Total_Users')}>{s(() => statistics.totalUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Active_Users')}>{s(() => statistics.activeUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Active_Guests')}>{s(() => statistics.activeGuests)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_App_Users')}>{s(() => statistics.appUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Non_Active_Users')}>{s(() => statistics.nonActiveUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Connected_Users')}>{s(() => statistics.totalConnectedUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Online_Users')}>{s(() => statistics.onlineUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Away_Users')}>{s(() => statistics.awayUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Offline_Users')}>{s(() => statistics.offlineUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Rooms')}>{s(() => statistics.totalRooms)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Channels')}>{s(() => statistics.totalChannels)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Private_Groups')}>{s(() => statistics.totalPrivateGroups)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Direct_Messages')}>{s(() => statistics.totalDirect)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Livechat_Rooms')}>{s(() => statistics.totalLivechat)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Total_Discussions')}>{s(() => statistics.totalDiscussions)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Total_Threads')}>{s(() => statistics.totalThreads)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages')}>{s(() => statistics.totalMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_Channel')}>{s(() => statistics.totalChannelMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_PrivateGroup')}>{s(() => statistics.totalPrivateGroupMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_Direct')}>{s(() => statistics.totalDirectMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_Livechat')}>{s(() => statistics.totalLivechatMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Uploads')}>{s(() => statistics.uploadsTotal)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Uploads_Size')}>{s(() => formatMemorySize(statistics.uploadsTotalSize))}</DescriptionList.Entry>
{statistics && statistics.apps && <>
<DescriptionList.Entry label={t('Stats_Total_Installed_Apps')}>{statistics.apps.totalInstalled}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Active_Apps')}>{statistics.apps.totalActive}</DescriptionList.Entry>
</>}
<DescriptionList.Entry label={t('Stats_Total_Integrations')}>{s(() => statistics.integrations.totalIntegrations)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncoming)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Active_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncomingActive)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoing)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Active_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoingActive)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -1,43 +0,0 @@
import moment from 'moment';
import s from 'underscore.string';
export const formatNumber = (number) => s.numberFormat(number, 2);
export const formatDate = (date) => {
if (!date) {
return null;
}
return moment(date).format('LLL');
};
export const formatHumanReadableTime = (time, t) => {
const days = Math.floor(time / 86400);
const hours = Math.floor((time % 86400) / 3600);
const minutes = Math.floor(((time % 86400) % 3600) / 60);
const seconds = Math.floor(((time % 86400) % 3600) % 60);
let out = '';
if (days > 0) {
out += `${ days } ${ t('days') }, `;
}
if (hours > 0) {
out += `${ hours } ${ t('hours') }, `;
}
if (minutes > 0) {
out += `${ minutes } ${ t('minutes') }, `;
}
if (seconds > 0) {
out += `${ seconds } ${ t('seconds') }`;
}
return out;
};
export const formatCPULoad = (load) => {
if (!load) {
return null;
}
const [oneMinute, fiveMinutes, fifteenMinutes] = load;
return `${ formatNumber(oneMinute) }, ${ formatNumber(fiveMinutes) }, ${ formatNumber(fifteenMinutes) }`;
};

@ -1,10 +0,0 @@
import { useEffect } from 'react';
import { SideNav } from '../../../ui-utils/client/lib/SideNav';
export const useAdminSideNav = () => {
useEffect(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}, []);
};

@ -1,102 +0,0 @@
<template name="adminRoomInfo">
{{#with selectedRoom}}
<section class="contextual-bar__content">
<div class="list-view channel-settings">
<div class="title">
<h2>{{_ "Room_Info"}}</h2>
</div>
<form>
<ul class="list clearfix">
{{#if notDirect}}
<li>
<label>{{_ "Name"}}</label>
<div>
{{#if editing 'roomName'}}
<input type="text" name="roomName" value="{{roomName}}" class="content-background-color editing" />
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{roomName}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomName"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{/if}}
{{#if roomOwner}}
<li>
<label>{{_ "Owner"}}</label>
<div>
<span>{{roomOwner}}</span>
</div>
</li>
{{/if}}
<li>
<label>{{_ "Topic"}}</label>
<div>
{{#if editing 'roomTopic'}}
<input type="text" name="roomTopic" value="{{roomTopic}}" class="content-background-color editing" />
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{{RocketChatMarkdown roomTopic}}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomTopic"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{#if notDirect}}
<li>
<label>{{_ "Type"}}</label>
<div>
{{#if editing 'roomType'}}
<label><input type="radio" name="roomType" class="editing" value="c" checked="{{$eq roomType 'c'}}" /> {{_ "Channel"}}</label>
<label><input type="radio" name="roomType" value="p" checked="{{$eq roomType 'p'}}" /> {{_ "Private_Group"}}</label>
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{roomTypeDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomType"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{/if}}
{{#if notDirect}}
<li>
<label>{{_ "Room_archivation_state"}}</label>
<div>
{{#if editing 'archivationState'}}
<label><input type="radio" name="archivationState" class="editing" value="true" checked="{{$eq archivationState true}}" /> {{_ "Room_archivation_state_true"}}</label>
<label><input type="radio" name="archivationState" value="false" checked="{{$neq archivationState true}}" /> {{_ "Room_archivation_state_false"}}</label>
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{archivationStateDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="archivationState"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{/if}}
{{#if notDirect}}
<li>
<label>{{_ "Read_only_channel"}}</label>
<div>
{{#if editing 'readOnly'}}
<label><input type="radio" name="readOnly" class="editing" value="true" checked="{{$eq readOnly true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="readOnly" value="false" checked="{{$neq readOnly true}}" /> {{_ "False"}}</label>
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{readOnlyDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{/if}}
{{#each channelSettings}}
{{> Template.dynamic template=template data=data}}
{{/each}}
</ul>
</form>
{{#if canDeleteRoom}}
<nav>
<button class='button danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button>
</nav>
{{/if}}
</div>
</section>
{{/with}}
</template>

@ -1,304 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import s from 'underscore.string';
import { t, handleError } from '../../../utils';
import { call, modal } from '../../../ui-utils';
import { hasAllPermission, hasAtLeastOnePermission } from '../../../authorization';
import { ChannelSettings } from '../../../channel-settings';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
Template.adminRoomInfo.helpers({
selectedRoom() {
return Session.get('adminRoomsSelected');
},
canEdit() {
return hasAllPermission('edit-room', this.rid);
},
editing(field) {
return Template.instance().editing.get() === field;
},
notDirect() {
const room = Template.instance().room.get();
return room && room.t !== 'd';
},
roomType() {
const room = Template.instance().room.get();
return room && room.t;
},
channelSettings() {
return ChannelSettings.getOptions(undefined, 'admin-room');
},
roomTypeDescription() {
const room = Template.instance().room.get();
const roomType = room && room.t;
if (roomType === 'c') {
return t('Channel');
} if (roomType === 'p') {
return t('Private_Group');
}
},
roomName() {
const room = Template.instance().room.get();
return room && room.name;
},
roomOwner() {
const roomOwner = Template.instance().roomOwner.get();
return roomOwner && (roomOwner.name || roomOwner.username);
},
roomTopic() {
const room = Template.instance().room.get();
return room && room.topic;
},
archivationState() {
const room = Template.instance().room.get();
return room && room.archived;
},
archivationStateDescription() {
const room = Template.instance().room.get();
const archivationState = room && room.archived;
if (archivationState === true) {
return t('Room_archivation_state_true');
}
return t('Room_archivation_state_false');
},
canDeleteRoom() {
const room = Template.instance().room.get();
const roomType = room && room.t;
return (roomType != null) && hasAtLeastOnePermission(`delete-${ roomType }`);
},
readOnly() {
const room = Template.instance().room.get();
return room && room.ro;
},
readOnlyDescription() {
const room = Template.instance().room.get();
const readOnly = room && room.ro;
if (readOnly === true) {
return t('True');
}
return t('False');
},
});
Template.adminRoomInfo.events({
'click .delete'(event, instance) {
modal.open({
title: t('Are_you_sure'),
text: t('Delete_Room_Warning'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, () => {
Meteor.call('eraseRoom', this.rid, function(error) {
if (error) {
handleError(error);
} else {
modal.open({
title: t('Deleted'),
text: t('Room_has_been_deleted'),
type: 'success',
timer: 2000,
showConfirmButton: false,
});
instance.onSuccess();
instance.data.tabBar.close();
}
});
});
},
'keydown input[type=text]'(e, t) {
if (e.keyCode === 13) {
e.preventDefault();
t.saveSetting(this.rid);
}
},
'click [data-edit]'(e, t) {
e.preventDefault();
t.editing.set($(e.currentTarget).data('edit'));
return setTimeout(function() {
t.$('input.editing').focus().select();
}, 100);
},
'click .cancel'(e, t) {
e.preventDefault();
t.editing.set();
},
'click .save'(e, t) {
e.preventDefault();
t.saveSetting(this.rid);
},
});
Template.adminRoomInfo.onCreated(function() {
const instance = this;
const currentData = Template.currentData();
this.editing = new ReactiveVar();
this.room = new ReactiveVar();
this.roomOwner = new ReactiveVar();
this.onSuccess = Template.currentData().onSuccess;
this.autorun(() => {
const { room } = Template.currentData();
this.room.set(room);
});
this.validateRoomType = () => {
const type = this.$('input[name=roomType]:checked').val();
if (type !== 'c' && type !== 'p') {
toastr.error(t('error-invalid-room-type', { type }));
}
return true;
};
this.validateRoomName = (rid) => {
const { room } = currentData;
let nameValidation;
if (!hasAllPermission('edit-room', rid) || (room.t !== 'c' && room.t !== 'p')) {
toastr.error(t('error-not-allowed'));
return false;
}
name = $('input[name=roomName]').val();
try {
nameValidation = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
} catch (_error) {
nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
}
if (!nameValidation.test(name)) {
toastr.error(t('error-invalid-room-name', {
room_name: s.escapeHTML(name),
}));
return false;
}
return true;
};
this.validateRoomTopic = () => true;
this.saveSetting = (rid) => {
switch (this.editing.get()) {
case 'roomName':
if (this.validateRoomName(rid)) {
callbacks.run('roomNameChanged', currentData.room);
Meteor.call('saveRoomSettings', rid, 'roomName', this.$('input[name=roomName]').val(), function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_name_changed_successfully'));
instance.onSuccess();
instance.data.tabBar.close();
});
}
break;
case 'roomTopic':
if (this.validateRoomTopic(rid)) {
Meteor.call('saveRoomSettings', rid, 'roomTopic', this.$('input[name=roomTopic]').val(), function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_topic_changed_successfully'));
callbacks.run('roomTopicChanged', currentData.room);
instance.onSuccess();
instance.data.tabBar.close();
});
}
break;
case 'roomAnnouncement':
if (this.validateRoomTopic(rid)) {
Meteor.call('saveRoomSettings', rid, 'roomAnnouncement', this.$('input[name=roomAnnouncement]').val(), function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_announcement_changed_successfully'));
callbacks.run('roomAnnouncementChanged', currentData.room);
instance.onSuccess();
instance.data.tabBar.close();
});
}
break;
case 'roomType':
const val = this.$('input[name=roomType]:checked').val();
if (this.validateRoomType(rid)) {
callbacks.run('roomTypeChanged', currentData.room);
const saveRoomSettings = function() {
Meteor.call('saveRoomSettings', rid, 'roomType', val, function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_type_changed_successfully'));
instance.onSuccess();
instance.data.tabBar.close();
});
};
if (!currentData.room.default) {
return saveRoomSettings();
}
modal.open({
title: t('Room_default_change_to_private_will_be_default_no_more'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes'),
cancelButtonText: t('Cancel'),
closeOnConfirm: true,
html: false,
}, function(confirmed) {
return !confirmed || saveRoomSettings();
});
}
break;
case 'archivationState':
const { room } = currentData;
if (this.$('input[name=archivationState]:checked').val() === 'true') {
if (room && room.archived !== true) {
Meteor.call('archiveRoom', rid, function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_archived'));
callbacks.run('archiveRoom', currentData.room);
instance.onSuccess();
instance.data.tabBar.close();
});
}
} else if ((room && room.archived) === true) {
Meteor.call('unarchiveRoom', rid, function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_unarchived'));
callbacks.run('unarchiveRoom', currentData.room);
instance.onSuccess();
instance.data.tabBar.close();
});
}
break;
case 'readOnly':
Meteor.call('saveRoomSettings', rid, 'readOnly', this.$('input[name=readOnly]:checked').val() === 'true', function(err) {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Read_only_changed_successfully'));
instance.onSuccess();
instance.data.tabBar.close();
});
}
this.editing.set();
};
this.autorun(async () => {
this.roomOwner.set(null);
for (const { roles, u } of await call('getRoomRoles', Session.get('adminRoomsSelected').rid)) {
if (roles.includes('owner')) {
this.roomOwner.set(u);
}
}
});
});

@ -1,78 +0,0 @@
<template name="adminRooms">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Rooms" fixedHeight="true" fullpage="true"}}
<div class="content">
{{#unless hasPermission 'view-room-administration'}}
<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 isLoading}}
{{> loading }}
{{else}}
{{> icon block="rc-input__icon-svg" icon="magnifier" }}
{{/if}}
</div>
<input id="rooms-filter" type="text" class="rc-input__element"
placeholder="{{_ "Search"}}" autofocus dir="auto">
</div>
<label><input type="checkbox" name="room-type" value="c"> {{_ "Channels"}}</label>
<label><input type="checkbox" name="room-type" value="d"> {{_ "Direct_Messages"}}</label>
<label><input type="checkbox" name="room-type" value="p"> {{_ "Private_Groups"}}</label>
<label><input type="checkbox" name="room-type" value="l"> {{_ "Omnichannel"}}</label>
<label><input type="checkbox" name="room-type" value="discussions"> {{_ "Discussions"}}</label>
</form>
<div class="results">
{{{_ "Showing_results" roomCount}}}
</div>
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr>
<th width="30%"><div class="table-fake-th">{{_ "Name"}}</div></th>
<th width="20%"><div class="table-fake-th">{{_ "Type"}}</div></th>
<th width="20%"><div class="table-fake-th">{{_ "Users"}}</div></th>
<th width="10%"><div class="table-fake-th">{{_ "Msgs"}}</div></th>
<th width="10%"><div class="table-fake-th">{{_ "Default"}}</div></th>
<th width="10%"><div class="table-fake-th">{{_ "Featured"}}</div></th>
</tr>
</thead>
<tbody>
{{#each rooms}}
<tr>
<td width="30%"><div class="rc-table-wrapper">
<div class="rc-table-avatar">{{> avatar url=url roomIcon="true"}}</div>
<div class="rc-table-info">
<span class="rc-table-title">
{{>icon icon=getIcon block="rc-table-icon"}} {{roomName}}
</span>
</div>
</div></td>
<td width="20%"><div class="rc-table-wrapper">{{type}}</div></td>
<td width="20%"><div class="rc-table-wrapper">{{usersCount}}</div></td>
<td width="10%"><div class="rc-table-wrapper">{{msgs}}</div></td>
<td width="10%"><div class="rc-table-wrapper">{{default}}</div></td>
<td width="10%"><div class="rc-table-wrapper">{{#if featured}}True{{else}}False{{/if}}</div></td>
</tr>
{{else}} {{# with searchText}}
<tr class="table-no-click">
<td>{{_ "No_results_found_for"}} {{.}}</td>
</tr>
{{/with}}
{{/each}}
{{#if isLoading}}
<tr class="table-no-click">
<td class="table-loading-td" colspan="{{#if showLastMessage}}5{{else}}4{{/if}}">{{> loading}}</td>
</tr>
{{/if}}
</tbody>
{{/table}}
{{/unless}}
</div>
</section>
{{#with flexData}}
{{> flexTabBar}}
{{/with}}
</div>
</template>

@ -1,210 +0,0 @@
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import { SideNav, RocketChatTabBar, TabBar } from '../../../ui-utils';
import { t, roomTypes } from '../../../utils';
import { hasAllPermission } from '../../../authorization';
import { ChannelSettings } from '../../../channel-settings';
import { getAvatarURL } from '../../../utils/lib/getAvatarURL';
import { APIClient } from '../../../utils/client';
const LIST_SIZE = 50;
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500;
Template.adminRooms.helpers({
url() {
return this.t === 'd' ? getAvatarURL({ username: `@${ this.usernames[0] }` }) : roomTypes.getConfig(this.t).getAvatarPath(this);
},
getIcon() {
return roomTypes.getIcon(this);
},
roomName() {
return this.t === 'd' ? this.usernames.join(' x ') : roomTypes.getRoomName(this.t, this);
},
searchText() {
const instance = Template.instance();
return instance.filter && instance.filter.get();
},
rooms() {
return Template.instance().rooms.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
roomCount() {
return Template.instance().rooms.get().length;
},
type() {
return TAPi18n.__(roomTypes.getConfig(this.t).label);
},
'default'() {
if (this.default) {
return t('True');
}
return t('False');
},
flexData() {
return {
tabBar: Template.instance().tabBar,
data: Template.instance().tabBarData.get(),
};
},
onTableScroll() {
const instance = Template.instance();
return function(currentTarget) {
if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) {
return;
}
const rooms = instance.rooms.get();
if (instance.total.get() > rooms.length) {
instance.offset.set(instance.offset.get() + LIST_SIZE);
}
};
},
onTableItemClick() {
const instance = Template.instance();
return function(item) {
Session.set('adminRoomsSelected', {
rid: item._id,
});
instance.tabBarData.set({
room: instance.rooms.get().find((room) => room._id === item._id),
onSuccess: instance.onSuccessCallback,
});
instance.tabBar.open('admin-room');
};
},
});
Template.adminRooms.onCreated(function() {
const instance = this;
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.filter = new ReactiveVar('');
this.types = new ReactiveVar([]);
this.rooms = new ReactiveVar([]);
this.ready = new ReactiveVar(true);
this.isLoading = new ReactiveVar(false);
this.tabBar = new RocketChatTabBar();
this.tabBarData = new ReactiveVar();
this.tabBar.showGroup(FlowRouter.current().route.name);
TabBar.addButton({
groups: ['admin-rooms'],
id: 'admin-room',
i18nTitle: 'Room_Info',
icon: 'info-circled',
template: 'adminRoomInfo',
order: 7,
});
ChannelSettings.addOption({
group: ['admin-room'],
id: 'make-default',
template: 'channelSettingsDefault',
data() {
return {
room: instance.tabBarData.get().room,
onSuccess: instance.tabBarData.get().onSuccess,
};
},
validation() {
return hasAllPermission('view-room-administration');
},
});
ChannelSettings.addOption({
group: ['admin-room'],
id: 'make-featured',
template: 'channelSettingsFeatured',
data() {
return {
room: instance.tabBarData.get().room,
onSuccess: instance.tabBarData.get().onSuccess,
};
},
validation() {
return hasAllPermission('view-room-administration');
},
});
this.onSuccessCallback = () => {
instance.offset.set(0);
return instance.loadRooms(instance.types.get(), instance.filter.get(), instance.offset.get());
};
this.tabBarData.set({
onSuccess: instance.onSuccessCallback,
});
const mountTypesQueryParameter = (types) => types.reduce((acc, item) => {
acc += `types[]=${ item }&`;
return acc;
}, '');
this.loadRooms = _.debounce(async (types = [], filter, offset) => {
this.isLoading.set(true);
let url = `rooms.adminRooms?count=${ LIST_SIZE }&offset=${ offset }&${ mountTypesQueryParameter(types) }`;
if (filter) {
url += `filter=${ encodeURIComponent(filter) }`;
}
const { rooms, total } = await APIClient.v1.get(url);
this.total.set(total);
if (offset === 0) {
this.rooms.set(rooms);
} else {
this.rooms.set(this.rooms.get().concat(rooms));
}
this.isLoading.set(false);
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS);
const allowedTypes = ['c', 'd', 'p'];
this.autorun(() => {
instance.filter.get();
instance.types.get();
instance.offset.set(0);
});
this.autorun(() => {
const filter = instance.filter.get();
const offset = instance.offset.get();
let types = instance.types.get();
if (types.length === 0) {
types = allowedTypes;
}
return this.loadRooms(types, filter, offset);
});
this.getSearchTypes = function() {
return _.map($('[name=room-type]:checked'), function(input) {
return $(input).val();
});
};
});
Template.adminRooms.onRendered(function() {
Tracker.afterFlush(function() {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});
Template.adminRooms.events({
'keydown #rooms-filter'(e) {
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
}
},
'keyup #rooms-filter'(e, t) {
e.stopPropagation();
e.preventDefault();
t.filter.set(e.currentTarget.value);
},
'change [name=room-type]'(e, t) {
t.types.set(t.getSearchTypes());
},
});

@ -1,20 +0,0 @@
<template name="channelSettingsDefault">
{{#if canMakeDefault}}
<li>
<label>{{_ "Default"}}</label>
<div>
{{#if editing 'default'}}
<label><input type="radio" name="default" class="editing" value="true" checked="{{$eq roomDefault true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="default" value="false" checked="{{$neq roomDefault true}}" /> {{_ "False"}}</label>
{{#if roomDefault}}
<label><input type="checkbox" name="favorite" checked="{{isFavorite}}" /> {{_ "Set_as_favorite"}}</label>
{{/if}}
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{defaultDescription}} <i class="icon-pencil" data-edit="default"></i></span>
{{/if}}
</div>
</li>
{{/if}}
</template>

@ -1,84 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import { t, handleError } from '../../../utils';
Template.channelSettingsDefault.helpers({
canMakeDefault() {
const room = Template.instance().room.get();
return room && room.t === 'c';
},
editing(field) {
return Template.instance().editing.get() === field;
},
roomDefault() {
return Template.instance().isDefault.get();
},
isFavorite() {
return Template.instance().isFavorite.get();
},
defaultDescription() {
const { room, isDefault, isFavorite } = { room: Template.instance().room.get(), isDefault: Template.instance().isDefault.get(), isFavorite: Template.instance().isFavorite.get() };
let description = t('False');
if (room && isDefault) {
description = t('True');
if (isFavorite) {
description += `, ${ t('Set_as_favorite') }`;
}
return description;
}
return description;
},
});
Template.channelSettingsDefault.events({
'change input[name=default]'(e, t) {
t.isDefault.set(e.currentTarget.value === 'true');
},
'change input[name=favorite]'(e, t) {
t.isFavorite.set(e.currentTarget.checked);
},
'click [data-edit]'(e, t) {
e.preventDefault();
t.editing.set($(e.currentTarget).data('edit'));
setTimeout(() => {
t.$('input.editing').focus().select();
}, 100);
},
'click .cancel'(e, t) {
e.preventDefault();
t.editing.set();
},
'click .save'(e, t) {
e.preventDefault();
Meteor.call('saveRoomSettings', t.room.get()._id, { default: t.isDefault.get(), favorite: { defaultValue: t.isDefault.get(), favorite: t.isFavorite.get() } }, (err/* , result*/) => {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_type_changed_successfully'));
});
t.onSuccess();
t.editing.set();
},
});
Template.channelSettingsDefault.onCreated(function() {
this.editing = new ReactiveVar();
this.isDefault = new ReactiveVar();
this.isFavorite = new ReactiveVar();
this.room = new ReactiveVar();
this.onSuccess = Template.currentData().onSuccess;
this.autorun(() => {
const { room } = Template.currentData();
this.isDefault.set(room && room.default);
this.isFavorite.set(room && room.favorite && room.default);
this.room.set(room);
});
});

@ -1,15 +0,0 @@
<template name="channelSettingsFeatured">
<li>
<label>{{_ "Featured"}}</label>
<div>
{{#if editing 'featured'}}
<label><input type="radio" name="featured" class="editing" value="true" checked="{{$eq roomFeatured true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="featured" value="false" checked="{{$neq roomFeatured true}}" /> {{_ "False"}}</label>
<button type="button" class="button cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{featuredDescription}} <i class="icon-pencil" data-edit="featured"></i></span>
{{/if}}
</div>
</li>
</template>

@ -1,65 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import './channelSettingsFeatured.html';
import { t, handleError } from '../../../utils';
Template.channelSettingsFeatured.helpers({
editing(field) {
return Template.instance().editing.get() === field;
},
roomFeatured() {
const room = Template.instance().room.get();
if (room) {
return room.featured;
}
},
featuredDescription() {
const room = Template.instance().room.get();
if (room && room.featured) {
return t('True');
}
return t('False');
},
});
Template.channelSettingsFeatured.events({
'click [data-edit]'(e, t) {
e.preventDefault();
t.editing.set($(e.currentTarget).data('edit'));
setTimeout(() => {
t.$('input.editing').focus().select();
}, 100);
},
'click .cancel'(e, t) {
e.preventDefault();
t.editing.set();
},
'click .save'(e, t) {
e.preventDefault();
Meteor.call('saveRoomSettings', Template.instance().room.get()._id, 'featured', $('input[name=featured]:checked').val(), (err/* , result*/) => {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Room_changed_successfully'));
});
t.onSuccess();
t.editing.set();
},
});
Template.channelSettingsFeatured.onCreated(function() {
this.editing = new ReactiveVar();
this.room = new ReactiveVar();
this.onSuccess = Template.currentData().onSuccess;
this.autorun(() => {
const { room } = Template.currentData();
this.room.set(room);
});
});

@ -1,7 +0,0 @@
import './adminRooms.html';
import './adminRoomInfo.html';
import './adminRoomInfo';
import './channelSettingsDefault.html';
import './channelSettingsDefault';
import './channelSettingsFeatured';
import './adminRooms';

@ -1,67 +0,0 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { Meteor } from 'meteor/meteor';
import { renderRouteComponent } from '../../../client/reactAdapters';
const routeGroup = FlowRouter.group({
name: 'admin',
prefix: '/admin',
});
export const registerAdminRoute = (path, { lazyRouteComponent, props, action, ...options } = {}) => {
routeGroup.route(path, {
...options,
action: (params, queryParams) => {
if (action) {
action(params, queryParams);
return;
}
renderRouteComponent(() => import('./components/AdministrationRouter'), {
template: 'main',
region: 'center',
propsFn: () => ({ lazyRouteComponent, ...options, params, queryParams, ...props }),
});
},
});
};
registerAdminRoute('/', {
triggersEnter: [(context, redirect) => {
redirect('admin-info');
}],
});
registerAdminRoute('/info', {
name: 'admin-info',
lazyRouteComponent: () => import('./components/info/InformationRoute'),
});
registerAdminRoute('/mailer', {
name: 'admin-mailer',
lazyRouteComponent: () => import('./components/mailer/MailerRoute'),
});
registerAdminRoute('/users', {
name: 'admin-users',
action: async () => {
await import('./users/views');
BlazeLayout.render('main', { center: 'adminUsers' });
},
});
registerAdminRoute('/rooms', {
name: 'admin-rooms',
action: async () => {
await import('./rooms/views');
BlazeLayout.render('main', { center: 'adminRooms' });
},
});
Meteor.startup(() => {
registerAdminRoute('/:group+', {
name: 'admin',
lazyRouteComponent: () => import('./components/settings/SettingsRoute'),
});
});

@ -1,10 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
const items = new ReactiveVar([]);
export const registerAdminSidebarItem = (itemOptions) => {
Tracker.nonreactive(() => items.set([...items.get(), itemOptions]));
};
export const getSidebarItems = () => items.get().filter((option) => !option.permissionGranted || option.permissionGranted());

@ -1,31 +0,0 @@
<template name="adminInviteUser">
{{#if isAllowed}}
<div class="content">
<div class="user-view">
<div class="about clearfix">
<form class="edit-form">
<h3>{{_ "Send_invitation_email"}}</h3>
<div class="input-line">
<label for="inviteEmails">{{_ "Send_invitation_email_info"}}</label>
<textarea id="inviteEmails" rows="3" style="height: auto" class="content-background-color"></textarea>
</div>
</form>
</div>
<nav>
<button class='button button-block cancel'><span>{{_ "Cancel"}}</span></button>
<button class='button button-block primary send' data-loading-text="{{_ "Please_wait"}}"><span>{{_ "Send"}}</span></button>
</nav>
{{#if inviteEmails.length}}
<div class="about clearfix" style="margin-top: 30px">
<p style="color: #51a351"> {{_ "Send_invitation_email_success"}} </p>
<ul style="margin: 5px 10px">
{{#each inviteEmails}}
<li style="margin-top: 5px">{{.}}</li>
{{/each}}
</ul>
</div>
{{/if}}
</div>
</div>
{{/if}}
</template>

@ -1,54 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import _ from 'underscore';
import toastr from 'toastr';
import { hasAtLeastOnePermission } from '../../../authorization';
import { t, handleError } from '../../../utils';
Template.adminInviteUser.helpers({
isAllowed() {
return hasAtLeastOnePermission('bulk-register-user');
},
inviteEmails() {
return Template.instance().inviteEmails.get();
},
});
Template.adminInviteUser.events({
'click .send'(e, instance) {
const emails = $('#inviteEmails').val().split(/[\s,;]/);
const rfcMailPattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const validEmails = _.compact(_.map(emails, function(email) {
if (rfcMailPattern.test(email)) {
return email;
}
}));
if (validEmails.length) {
Meteor.call('sendInvitationEmail', validEmails, function(error, result) {
if (result) {
instance.clearForm();
instance.inviteEmails.set(validEmails);
}
if (error) {
handleError(error);
}
});
} else {
toastr.error(t('Send_invitation_email_error'));
}
},
'click .cancel'(e, instance) {
instance.clearForm();
instance.inviteEmails.set([]);
Template.currentData().tabBar.close();
},
});
Template.adminInviteUser.onCreated(function() {
this.inviteEmails = new ReactiveVar([]);
this.clearForm = function() {
$('#inviteEmails').val('');
};
});

@ -1,3 +0,0 @@
<template name="adminUserEdit">
{{> userEdit .}}
</template>

@ -1,9 +0,0 @@
<template name="adminUserInfo">
{{#if _id}}
{{> userInfo .}}
{{else}}
<section class="contextual-bar__content">
<div>{{_ "Please_select_an_user"}}</div>
</section>
{{/if}}
</template>

@ -1,101 +0,0 @@
<template name="adminUsers">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Users"}}
<div class="content">
{{#unless hasPermission 'view-user-administration'}}
<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="users-filter" type="text" class="rc-input__element"
placeholder="{{_ "Search"}}" autofocus dir="auto">
</div>
</form>
<div class="results">
{{{_ "Showing_results" users.length}}}
</div>
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr>
<th width="34%">
<div class="table-fake-th">{{_ "Name"}}</div>
</th>
<th width="33%">
<div class="table-fake-th">{{_ "Username"}}</div>
</th>
<th width="33%">
<div class="table-fake-th">{{_ "Email"}}</div>
</th>
<th width="33%">
<div class="table-fake-th">{{_ "Roles"}}</div>
</th>
<th width="33%">
<div class="table-fake-th">{{_ "Status"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each users}}
<tr class='user-info'>
<td width="30%">
<div class="rc-table-wrapper">
<div class="rc-table-avatar">
{{> avatar username=username}}
</div>
<div class="rc-table-info">
<span class="rc-table-title">{{name}}</span>
</div>
</div>
</td>
<td width="20%">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{username}}</span>
</div>
</div>
</td>
<td width="20%">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{emailAddress}}</span>
</div>
</div>
</td>
<td width="10%">
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">{{roles}}</span>
</div>
</div>
</td>
<td width="20%">
<div class="rc-table-wrapper">{{#if $not active}}{{_"deactivated"}}{{else}}{{status}}{{/if}}</div>
</td>
</tr>
{{else}} {{# with searchText}}
<tr class="table-no-click">
<td>{{_ "No_results_found_for"}} {{.}}</td>
</tr>
{{/with}} {{/each}} {{#unless isReady}}
<tr class="table-no-click">
<td class="table-loading-td" colspan="{{#if showLastMessage}}5{{else}}4{{/if}}">{{> loading}}</td>
</tr>
{{/unless}}
</tbody>
{{/table}}
{{/unless}}
</div>
</section>
{{#with flexData}}
{{> flexTabBar}}
{{/with}}
</div>
</template>

@ -1,183 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import _ from 'underscore';
import { SideNav, TabBar, RocketChatTabBar } from '../../../ui-utils';
import { APIClient } from '../../../utils/client';
const USERS_COUNT = 50;
Template.adminUsers.helpers({
searchText() {
const instance = Template.instance();
return instance.filter && instance.filter.get();
},
isReady() {
const instance = Template.instance();
return instance.ready && instance.ready.get();
},
users() {
return Template.instance().users.get();
},
isLoading() {
const instance = Template.instance();
if (!(instance.ready && instance.ready.get())) {
return 'btn-loading';
}
},
hasMore() {
const instance = Template.instance();
const users = instance.users();
if (instance.offset && instance.offset.get() && users && users.length) {
return instance.offset.get() === users.length;
}
},
emailAddress() {
return _.map(this.emails, function(e) { return e.address; }).join(', ');
},
flexData() {
return {
tabBar: Template.instance().tabBar,
data: Template.instance().tabBarData.get(),
};
},
onTableScroll() {
const instance = Template.instance();
return function(currentTarget) {
if (
currentTarget.offsetHeight + currentTarget.scrollTop
>= currentTarget.scrollHeight - 100
) {
return instance.offset.set(instance.offset.get() + USERS_COUNT);
}
};
},
// onTableItemClick() {
// const instance = Template.instance();
// return function(item) {
// Session.set('adminRoomsSelected', {
// rid: item._id,
// });
// instance.tabBar.open('admin-room');
// };
// },
});
Template.adminUsers.onCreated(function() {
const instance = this;
this.offset = new ReactiveVar(0);
this.filter = new ReactiveVar('');
this.ready = new ReactiveVar(true);
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
this.tabBarData = new ReactiveVar();
this.users = new ReactiveVar([]);
TabBar.addButton({
groups: ['admin-users'],
id: 'admin-user-info',
i18nTitle: 'User_Info',
icon: 'user',
template: 'adminUserInfo',
order: 3,
});
TabBar.addButton({
groups: ['admin-users'],
id: 'invite-user',
i18nTitle: 'Invite_Users',
icon: 'send',
template: 'adminInviteUser',
order: 1,
});
TabBar.addButton({
groups: ['admin-users'],
id: 'add-user',
i18nTitle: 'Add_User',
icon: 'plus',
template: 'adminUserEdit',
order: 2,
});
this.loadUsers = async (filter, offset) => {
this.ready.set(false);
const query = {
$or: [
{ 'emails.address': { $regex: filter, $options: 'i' } },
{ username: { $regex: filter, $options: 'i' } },
{ name: { $regex: filter, $options: 'i' } },
],
};
let url = `users.list?count=${ USERS_COUNT }&offset=${ offset }`;
if (filter) {
url += `&query=${ JSON.stringify(query) }`;
}
const { users } = await APIClient.v1.get(url);
if (offset === 0) {
this.users.set(users);
} else {
this.users.set(this.users.get().concat(users));
}
this.ready.set(true);
};
this.autorun(async () => {
const filter = instance.filter.get();
const offset = instance.offset.get();
this.loadUsers(filter, offset);
});
});
Template.adminUsers.onRendered(function() {
Tracker.afterFlush(function() {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});
const DEBOUNCE_TIME_FOR_SEARCH_USERS_IN_MS = 300;
Template.adminUsers.events({
'keydown #users-filter'(e) {
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
}
},
'keyup #users-filter': _.debounce((e, t) => {
e.stopPropagation();
e.preventDefault();
t.filter.set(e.currentTarget.value);
t.offset.set(0);
}, DEBOUNCE_TIME_FOR_SEARCH_USERS_IN_MS),
'click .user-info'(e, instance) {
e.preventDefault();
instance.tabBarData.set({
...instance.users.get().find((user) => user._id === this._id),
onChange() {
const filter = instance.filter.get();
const offset = instance.offset.get();
instance.loadUsers(filter, offset);
},
});
instance.tabBar.open('admin-user-info');
},
'click .info-tabs button'(e) {
e.preventDefault();
$('.info-tabs button').removeClass('active');
$(e.currentTarget).addClass('active');
$('.user-info-content').hide();
$($(e.currentTarget).attr('href')).show();
},
'click .load-more'(e, t) {
e.preventDefault();
e.stopPropagation();
t.offset.set(t.offset.get() + USERS_COUNT);
},
});

@ -1,6 +0,0 @@
import './adminInviteUser.html';
import './adminUserEdit.html';
import './adminUserInfo.html';
import './adminUsers.html';
import './adminInviteUser';
import './adminUsers';

@ -2,6 +2,7 @@ import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/
import { UiKitMessage, UiKitComponent, kitContext, messageParser } from '@rocket.chat/fuselage-ui-kit';
import React, { useRef, useEffect } from 'react';
import RawText from '../../../../client/components/basic/RawText';
import { renderMessageBody } from '../../../ui-utils/client';
import * as ActionManager from '../ActionManager';
@ -11,7 +12,7 @@ messageParser.text = ({ text, type } = {}) => {
return text;
}
return <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />;
return <RawText>{renderMessageBody({ msg: text })}</RawText>;
};
export function MessageBlock({ mid: _mid, rid, blocks, appId }) {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save