Merge branch 'develop' into color-variables

pull/7748/head
Karl Prieb 8 years ago
commit e53a977f2a
  1. 2
      .docker/Dockerfile
  2. 1
      .gitignore
  3. 1
      .meteor/packages
  4. 3
      .meteor/versions
  5. 2
      .sandstorm/sandstorm-pkgdef.capnp
  6. 2
      .travis/snap.sh
  7. 153
      HISTORY.md
  8. 32
      example-build-run.sh
  9. 16
      example-build.sh
  10. 2
      package.json
  11. 28
      packages/rocketchat-analytics/client/loadScript.js
  12. 2
      packages/rocketchat-analytics/client/trackEvents.js
  13. 26
      packages/rocketchat-analytics/server/settings.js
  14. 11
      packages/rocketchat-api/server/v1/channels.js
  15. 11
      packages/rocketchat-api/server/v1/users.js
  16. 14
      packages/rocketchat-authorization/client/usersNameChanged.js
  17. 1
      packages/rocketchat-authorization/package.js
  18. 6
      packages/rocketchat-emoji-emojione/callbacks.js
  19. 5
      packages/rocketchat-emoji-emojione/package.js
  20. 2
      packages/rocketchat-emoji/package.js
  21. 3
      packages/rocketchat-file-upload/server/methods/sendFileMessage.js
  22. 8
      packages/rocketchat-i18n/i18n/de.i18n.json
  23. 20
      packages/rocketchat-i18n/i18n/en.i18n.json
  24. 3
      packages/rocketchat-i18n/i18n/es.i18n.json
  25. 1
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  26. 3
      packages/rocketchat-i18n/i18n/sq.i18n.json
  27. 4
      packages/rocketchat-integrations/server/lib/triggerHandler.js
  28. 4
      packages/rocketchat-lib/client/Notifications.js
  29. 5
      packages/rocketchat-lib/client/lib/roomExit.js
  30. 2
      packages/rocketchat-lib/package.js
  31. 2
      packages/rocketchat-lib/rocketchat.info
  32. 54
      packages/rocketchat-lib/server/functions/saveCustomFields.js
  33. 33
      packages/rocketchat-lib/server/functions/saveCustomFieldsWithoutValidation.js
  34. 5
      packages/rocketchat-lib/server/functions/sendMessage.js
  35. 2
      packages/rocketchat-lib/server/functions/updateMessage.js
  36. 39
      packages/rocketchat-lib/server/functions/validateCustomFields.js
  37. 24
      packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js
  38. 15
      packages/rocketchat-lib/server/methods/getRoomRoles.js
  39. 3
      packages/rocketchat-lib/server/models/Users.js
  40. 21
      packages/rocketchat-lib/server/startup/settings.js
  41. 5
      packages/rocketchat-livechat/app/client/lib/_visitor.js
  42. 4
      packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.js
  43. 2
      packages/rocketchat-livechat/server/lib/Livechat.js
  44. 3
      packages/rocketchat-mentions-flextab/client/actionButton.js
  45. 1
      packages/rocketchat-mentions/Mentions.js
  46. 3
      packages/rocketchat-message-pin/client/actionButton.js
  47. 3
      packages/rocketchat-message-star/client/actionButton.js
  48. 2
      packages/rocketchat-oembed/server/server.js
  49. 24
      packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js
  50. 0
      packages/rocketchat-slider/README.md
  51. 16
      packages/rocketchat-slider/package.js
  52. 6
      packages/rocketchat-slider/rocketchat-slider.html
  53. 17
      packages/rocketchat-slider/rocketchat-slider.js
  54. 18
      packages/rocketchat-theme/client/imports/chip.css
  55. 82
      packages/rocketchat-theme/client/imports/general/base_old.css
  56. 61
      packages/rocketchat-theme/client/imports/slider.css
  57. 22
      packages/rocketchat-theme/client/main.css
  58. 64
      packages/rocketchat-theme/server/colors.less
  59. 6
      packages/rocketchat-ui-account/client/accountPreferences.html
  60. 6
      packages/rocketchat-ui-account/client/accountPreferences.js
  61. 4
      packages/rocketchat-ui-admin/client/rooms/adminRooms.js
  62. 3
      packages/rocketchat-ui-flextab/client/tabs/messageSearch.js
  63. 25
      packages/rocketchat-ui-flextab/client/tabs/userEdit.html
  64. 59
      packages/rocketchat-ui-flextab/client/tabs/userEdit.js
  65. 1
      packages/rocketchat-ui-message/client/popup/messagePopupConfig.html
  66. 41
      packages/rocketchat-ui-message/client/popup/messagePopupConfig.js
  67. 9
      packages/rocketchat-ui/client/lib/RoomHistoryManager.js
  68. 10
      packages/rocketchat-ui/client/lib/chatMessages.js
  69. 37
      packages/rocketchat-ui/client/lib/notification.js
  70. 33
      packages/rocketchat-ui/client/views/app/room.html
  71. 92
      packages/rocketchat-ui/client/views/app/room.js
  72. 10
      server/methods/createDirectMessage.js
  73. 32
      server/methods/getRoomNameById.js
  74. 4
      server/methods/saveUserPreferences.js
  75. 2
      server/methods/saveUserProfile.js
  76. 1
      server/publications/room.js
  77. 3
      server/startup/avatar.js
  78. 28
      tests/data/custom-fields.js
  79. 330
      tests/end-to-end/api/01-users.js
  80. 8
      tests/end-to-end/ui/11-admin.js
  81. 1
      tests/end-to-end/ui/13-permissions.js
  82. 13
      tests/pageobjects/flex-tab.page.js

@ -1,6 +1,6 @@
FROM rocketchat/base:4
ENV RC_VERSION 0.57.0-develop
ENV RC_VERSION 0.58.0-develop
MAINTAINER buildmaster@rocket.chat

1
.gitignore vendored

@ -36,6 +36,7 @@
.env
.externalToolBuilders
.idea
.vscode
.loadpath
.map
.metadata

@ -110,6 +110,7 @@ rocketchat:slashcommands-mute
rocketchat:slashcommands-open
rocketchat:slashcommands-topic
rocketchat:slashcommands-unarchive
rocketchat:slider
rocketchat:smarsh-connector
rocketchat:spotify
rocketchat:statistics

@ -104,7 +104,7 @@ oauth1@1.1.11
oauth2@1.1.11
observe-sequence@1.0.16
ordered-dict@1.0.9
ostrio:cookies@2.2.1
ostrio:cookies@2.2.2
pauli:accounts-linkedin@2.1.3
pauli:linkedin-oauth@1.1.0
percolate:synced-cron@1.3.2
@ -202,6 +202,7 @@ rocketchat:slashcommands-mute@0.0.1
rocketchat:slashcommands-open@0.0.1
rocketchat:slashcommands-topic@0.0.1
rocketchat:slashcommands-unarchive@0.0.1
rocketchat:slider@0.0.1
rocketchat:smarsh-connector@0.0.1
rocketchat:sms@0.0.1
rocketchat:spotify@0.0.1

@ -21,7 +21,7 @@ const pkgdef :Spk.PackageDefinition = (
appVersion = 62, # Increment this for every release.
appMarketingVersion = (defaultText = "0.57.0-develop"),
appMarketingVersion = (defaultText = "0.58.0-develop"),
# Human-readable representation of appVersion. Should match the way you
# identify versions of your app in documentation and marketing.

@ -17,7 +17,7 @@ elif [[ $TRAVIS_TAG ]]; then
RC_VERSION=$TRAVIS_TAG
else
CHANNEL=edge
RC_VERSION=0.57.0-develop
RC_VERSION=0.58.0-develop
fi
echo "Preparing to trigger a snap release for $CHANNEL channel"

@ -1,5 +1,137 @@
<a name="0.57.0-rc.3"></a>
# 0.57.0-rc.3 (2017-06-28)
<a name="0.57.2"></a>
## 0.57.2 (2017-07-14)
### Bug Fixes
- [#7472](https://github.com/RocketChat/Rocket.Chat/pull/7472) Always set LDAP properties on login
- [#7431](https://github.com/RocketChat/Rocket.Chat/pull/7431) Fix Emails in User Admin View
- [#7469](https://github.com/RocketChat/Rocket.Chat/pull/7469) Fix file upload on Slack import
- [#7432](https://github.com/RocketChat/Rocket.Chat/pull/7432) Fix Private Channel List Submit
- [#7403](https://github.com/RocketChat/Rocket.Chat/pull/7403) Fix Unread Bar Disappearing
- [#7443](https://github.com/RocketChat/Rocket.Chat/pull/7443) S3 uploads not working for custom URLs
<a name="0.57.1"></a>
## 0.57.1 (2017-07-06)
- :hand: [#7428](https://github.com/RocketChat/Rocket.Chat/pull/7428) Fix migration of avatars from version 0.57.0
<details>
<summary>Others</summary>
- [#7428](https://github.com/RocketChat/Rocket.Chat/pull/7428) Run avatar migration on startup
</details>
<a name="0.57.0"></a>
# 0.57.0 (2017-07-03)
### Breaking Changes
- :hand: [#7095](https://github.com/RocketChat/Rocket.Chat/pull/7095) Internal hubot does not load [hubot-scripts](https://github.com/github/hubot-scripts) anymore.
### New Features
- [#7085](https://github.com/RocketChat/Rocket.Chat/pull/7085) API method and REST Endpoint for getting a single message by id
- [#6919](https://github.com/RocketChat/Rocket.Chat/pull/6919) Feature/delete any message permission
- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release
- [#7059](https://github.com/RocketChat/Rocket.Chat/pull/7059) Increase unread message count on [@here](https://github.com/here) mention
- [#6921](https://github.com/RocketChat/Rocket.Chat/pull/6921) LDAP: Use variables in User_Data_FieldMap for name mapping
- [#6857](https://github.com/RocketChat/Rocket.Chat/pull/6857) Make channel/group delete call answer to roomName
- [#7080](https://github.com/RocketChat/Rocket.Chat/pull/7080) Migration to add <html> tags to email header and footer
- [#6788](https://github.com/RocketChat/Rocket.Chat/pull/6788) New avatar storage types
- [#6690](https://github.com/RocketChat/Rocket.Chat/pull/6690) Show full name in mentions if use full name setting enabled
- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page
- [#6605](https://github.com/RocketChat/Rocket.Chat/pull/6605) Start running unit tests
- [#7311](https://github.com/RocketChat/Rocket.Chat/pull/7311) Force use of MongoDB for spotlight queries
### Bug Fixes
- [#7025](https://github.com/RocketChat/Rocket.Chat/pull/7025) Add <html> and </html> to header and footer
- [#7084](https://github.com/RocketChat/Rocket.Chat/pull/7084) Add option to ignore TLS in SMTP server settings
- [#7072](https://github.com/RocketChat/Rocket.Chat/pull/7072) Add support for carriage return in markdown code blocks
- [#6910](https://github.com/RocketChat/Rocket.Chat/pull/6910) Allow image insert from slack through slackbridge
- [#6904](https://github.com/RocketChat/Rocket.Chat/pull/6904) Bugs in `isUserFromParams` helper
- [#6840](https://github.com/RocketChat/Rocket.Chat/pull/6840) Check that username is not in the room when being muted / unmuted
- [#7103](https://github.com/RocketChat/Rocket.Chat/pull/7103) clipboard (permalink, copy, pin, star buttons)
- [#7030](https://github.com/RocketChat/Rocket.Chat/pull/7030) do only store password if LDAP_Login_Fallback is on
- [#7105](https://github.com/RocketChat/Rocket.Chat/pull/7105) edit button on firefox
- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype
- [#7045](https://github.com/RocketChat/Rocket.Chat/pull/7045) Fix avatar upload via users.setAvatar REST endpoint
- [#6950](https://github.com/RocketChat/Rocket.Chat/pull/6950) Fix badge counter on iOS push notifications
- [#7121](https://github.com/RocketChat/Rocket.Chat/pull/7121) fix bug in preview image
- [#6972](https://github.com/RocketChat/Rocket.Chat/pull/6972) Fix error handling for non-valid avatar URL
- [#6974](https://github.com/RocketChat/Rocket.Chat/pull/6974) Fix login with Meteor saving an object as email address
- [#7104](https://github.com/RocketChat/Rocket.Chat/pull/7104) Fix missing CSS files on production builds
- [#6986](https://github.com/RocketChat/Rocket.Chat/pull/6986) Fix the other tests failing due chimp update
- [#7049](https://github.com/RocketChat/Rocket.Chat/pull/7049) Improve Tests
- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c
- [#7044](https://github.com/RocketChat/Rocket.Chat/pull/7044) New screen sharing Chrome extension checking method
- [#6999](https://github.com/RocketChat/Rocket.Chat/pull/6999) overlapping text for users-typing-message
- [#7014](https://github.com/RocketChat/Rocket.Chat/pull/7014) Parse HTML on admin setting's descriptions
- [#6997](https://github.com/RocketChat/Rocket.Chat/pull/6997) Parse markdown links last
- [#7033](https://github.com/RocketChat/Rocket.Chat/pull/7033) Prevent Ctrl key on message field from reloading messages list
- [#6912](https://github.com/RocketChat/Rocket.Chat/pull/6912) Remove room from roomPick setting
- [#6961](https://github.com/RocketChat/Rocket.Chat/pull/6961) SAML: Only set KeyDescriptor when non empty
- [#7023](https://github.com/RocketChat/Rocket.Chat/pull/7023) Sidenav roomlist
- [#6913](https://github.com/RocketChat/Rocket.Chat/pull/6913) Slackbridge text replacements
- [#6903](https://github.com/RocketChat/Rocket.Chat/pull/6903) Updating Incoming Integration Post As Field Not Allowed
- [#6947](https://github.com/RocketChat/Rocket.Chat/pull/6947) Use AWS Signature Version 4 signed URLs for uploads
- [#7012](https://github.com/RocketChat/Rocket.Chat/pull/7012) video message recording dialog is shown in an incorrect position
- [#7157](https://github.com/RocketChat/Rocket.Chat/pull/7157) Fix all reactions having the same username
- [#7215](https://github.com/RocketChat/Rocket.Chat/pull/7215/) Fix the Zapier oAuth return url to the new one
- [#7209](https://github.com/RocketChat/Rocket.Chat/pull/7209) "requirePasswordChange" property not being saved when set to false
- [#7208](https://github.com/RocketChat/Rocket.Chat/pull/7208) Fix oembed previews not being shown
- [#7200](https://github.com/RocketChat/Rocket.Chat/pull/7200) Fix editing others messages
- [#7160](https://github.com/RocketChat/Rocket.Chat/pull/7160) Removing the kadira package install from example build script.
- [#7345](https://github.com/RocketChat/Rocket.Chat/pull/7345) click on image in a message
- [#7207](https://github.com/RocketChat/Rocket.Chat/pull/7207) Fix Block Delete Message After (n) Minutes
- [#7320](https://github.com/RocketChat/Rocket.Chat/pull/7320) Fix jump to unread button
- [#7321](https://github.com/RocketChat/Rocket.Chat/pull/7321) Fix Secret Url
- [#7358](https://github.com/RocketChat/Rocket.Chat/pull/7358) Fix user's customFields not being saved correctly
- [#7352](https://github.com/RocketChat/Rocket.Chat/pull/7352) Improve avatar migration
- [#7304](https://github.com/RocketChat/Rocket.Chat/pull/7304) Proxy upload to correct instance
- [#7379](https://github.com/RocketChat/Rocket.Chat/pull/7379) Message being displayed unescaped
<details>
<summary>Others</summary>
- [#7094](https://github.com/RocketChat/Rocket.Chat/pull/7094) [FIX]Fix the failing tests
- [#7092](https://github.com/RocketChat/Rocket.Chat/pull/7092) [FIX]Fixed typo hmtl -> html
- [#7145](https://github.com/RocketChat/Rocket.Chat/pull/7145) Convert file unsubscribe.coffee to js
- [#7146](https://github.com/RocketChat/Rocket.Chat/pull/7146) Convert hipchat importer to js
- [#7022](https://github.com/RocketChat/Rocket.Chat/pull/7022) Convert irc package to js
- [#7096](https://github.com/RocketChat/Rocket.Chat/pull/7096) Convert Livechat from Coffeescript to JavaScript
- [#6936](https://github.com/RocketChat/Rocket.Chat/pull/6936) Convert meteor-autocomplete package to js
- [#7017](https://github.com/RocketChat/Rocket.Chat/pull/7017) Convert oauth2-server-config package to js
- [#6795](https://github.com/RocketChat/Rocket.Chat/pull/6795) Convert Ui Account Package to Js
- [#6911](https://github.com/RocketChat/Rocket.Chat/pull/6911) Convert ui-admin package to js
- [#6775](https://github.com/RocketChat/Rocket.Chat/pull/6775) Convert WebRTC Package to Js
- [#7018](https://github.com/RocketChat/Rocket.Chat/pull/7018) converted rocketchat-importer
- [#6836](https://github.com/RocketChat/Rocket.Chat/pull/6836) converted rocketchat-ui coffee to js part 2
- [#6976](https://github.com/RocketChat/Rocket.Chat/pull/6976) fix the crashing tests
- [#7055](https://github.com/RocketChat/Rocket.Chat/pull/7055) Ldap: User_Data_FieldMap description
- [#7114](https://github.com/RocketChat/Rocket.Chat/pull/7114) LingoHub based on develop
- [#7005](https://github.com/RocketChat/Rocket.Chat/pull/7005) LingoHub based on develop
- [#6978](https://github.com/RocketChat/Rocket.Chat/pull/6978) LingoHub based on develop
- [#7062](https://github.com/RocketChat/Rocket.Chat/pull/7062) Remove Useless Jasmine Tests
- [#6914](https://github.com/RocketChat/Rocket.Chat/pull/6914) Rocketchat ui message
- [#7006](https://github.com/RocketChat/Rocket.Chat/pull/7006) Rocketchat ui3
- [#6987](https://github.com/RocketChat/Rocket.Chat/pull/6987) rocketchat-importer-slack coffee to js
- [#6735](https://github.com/RocketChat/Rocket.Chat/pull/6735) rocketchat-lib[4] coffee to js
- [#7154](https://github.com/RocketChat/Rocket.Chat/pull/7154) Remove missing CoffeeScript dependencies
- [#7308](https://github.com/RocketChat/Rocket.Chat/pull/7308) Escape error messages
- [#7102](https://github.com/RocketChat/Rocket.Chat/pull/7102) add server methods getRoomNameById
</details>
<details>
<summary>Details</summary>
## 0.57.0-rc.3 (2017-06-28)
### New Features
@ -26,8 +158,7 @@
<a name="0.57.0-rc.2"></a>
# 0.57.0-rc.2 (2017-06-12)
## 0.57.0-rc.2 (2017-06-12)
### Bug Fixes
@ -40,8 +171,7 @@
<a name="0.57.0-rc.1"></a>
# 0.57.0-rc.1 (2017-06-02)
## 0.57.0-rc.1 (2017-06-02)
### Bug Fixes
@ -57,8 +187,7 @@
<a name="0.57.0-rc.0"></a>
# 0.57.0-rc.0 (2017-06-01)
## 0.57.0-rc.0 (2017-06-01)
### New Features
@ -139,14 +268,10 @@
- [#6735](https://github.com/RocketChat/Rocket.Chat/pull/6735) rocketchat-lib[4] coffee to js
</details>
</details>
<a name="0.56.0"></a>
# NEXT
### Breaking Changes
- [#7095](https://github.com/RocketChat/Rocket.Chat/pull/7095) Internal hubot does not load [hubot-scripts](https://github.com/github/hubot-scripts) anymore.
<a name="0.56.0"></a>
# 0.56.0 (2017-05-15)
### New Features

@ -0,0 +1,32 @@
#!/bin/bash
set -x
set -euvo pipefail
IFS=$'\n\t'
# Requies Node.js version 4.x
# Do not run as root
DEPLOY_DIR=/var/www/rocket.chat
### BUILD
meteor npm install
# on the very first build, meteor build command should fail due to a bug on emojione package (related to phantomjs installation)
# the command below forces the error to happen before build command (not needed on subsequent builds)
set +e
meteor add rocketchat:lib
set -e
meteor build --server-only --directory $DEPLOY_DIR
### RUN
cd $DEPLOY_DIR/bundle/programs/server
npm install
cd $DEPLOY_DIR/bundle
NODE_ENV=production \
PORT=3000 \
ROOT_URL=http://localhost:3000 \
MONGO_URL=mongodb://localhost:27017/rocketchat \
MONGO_OPLOG_URL=mongodb://localhost:27017/local \
node main.js

@ -1,16 +0,0 @@
#!/bin/bash
set -x
set -euvo pipefail
IFS=$'\n\t'
# Build
export NODE_ENV=production
meteor add rocketchat:internal-hubot
meteor build --server https://demo.rocket.chat --directory /var/www/rocket.chat
# Run
export METEOR_SETTINGS=$(cat settings.json)
cd /var/www/rocket.chat/bundle/programs/server
npm install
cd /var/www/rocket.chat/current
pm2 startOrRestart /var/www/rocket.chat/current/pm2.json

@ -1,7 +1,7 @@
{
"name": "Rocket.Chat",
"description": "The Ultimate Open Source WebChat Platform",
"version": "0.57.0-develop",
"version": "0.58.0-develop",
"author": {
"name": "Rocket.Chat",
"url": "https://rocket.chat/"

@ -2,6 +2,10 @@ Template.body.onRendered(() => {
Tracker.autorun((c) => {
const piwikUrl = RocketChat.settings.get('PiwikAnalytics_enabled') && RocketChat.settings.get('PiwikAnalytics_url');
const piwikSiteId = piwikUrl && RocketChat.settings.get('PiwikAnalytics_siteId');
const piwikPrependDomain = piwikUrl && RocketChat.settings.get('PiwikAnalytics_prependDomain');
const piwikCookieDomain = piwikUrl && RocketChat.settings.get('PiwikAnalytics_cookieDomain');
const piwikDomains = piwikUrl && RocketChat.settings.get('PiwikAnalytics_domains');
const piwikAdditionalTracker = piwikUrl && RocketChat.settings.get('PiwikAdditionalTrackers');
const googleId = RocketChat.settings.get('GoogleAnalytics_enabled') && RocketChat.settings.get('GoogleAnalytics_ID');
if (piwikSiteId || googleId) {
c.stop();
@ -14,7 +18,31 @@ Template.body.onRendered(() => {
window._paq.push(['trackPageView']);
window._paq.push(['enableLinkTracking']);
if (piwikPrependDomain) {
window._paq.push(['setDocumentTitle', `${ window.location.hostname }/${ document.title }`]);
}
const upperLevelDomain = `*.${ window.location.hostname.split('.').slice(1).join('.') }`;
if (piwikCookieDomain) {
window._paq.push(['setCookieDomain', upperLevelDomain]);
}
if (piwikDomains) {
// array
const domainsArray = piwikDomains.split(/\n/);
const domains = [];
for (let i = 0; i < domainsArray.length; i++) {
// only push domain if it contains a non whitespace character.
if (/\S/.test(domainsArray[i])) {
domains.push(`*.${ domainsArray[i].trim() }`);
}
}
window._paq.push(['setDomains', domains]);
}
(() => {
const addTrackers = JSON.parse(piwikAdditionalTracker);
for (let i = 0; i < addTrackers.length; i++) {
const tracker = addTrackers[i];
window._paq.push(['addTracker', `${ tracker['trackerURL'] }piwik.php`, tracker['siteId']]);
}
window._paq.push(['setTrackerUrl', `${ piwikUrl }piwik.php`]);
window._paq.push(['setSiteId', Number.parseInt(piwikSiteId)]);
const d = document;

@ -8,7 +8,7 @@ function trackEvent(category, action, label) {
}
if (!window._paq || window.ga) {
//Trigger the trackPageView manually as the page views don't seem to be tracked
//Trigger the trackPageView manually as the page views are only loaded when the loadScript.js code is executed
FlowRouter.triggers.enter([(route) => {
if (window._paq) {
const http = location.protocol;

@ -18,6 +18,32 @@ RocketChat.settings.addGroup('Analytics', function addSettings() {
i18nLabel: 'Client_ID',
enableQuery
});
this.add('PiwikAdditionalTrackers', '', {
type: 'string',
multiline: true,
public: true,
i18nLabel: 'PiwikAdditionalTrackers',
enableQuery
});
this.add('PiwikAnalytics_prependDomain', false, {
type: 'boolean',
public: true,
i18nLabel: 'PiwikAnalytics_prependDomain',
enableQuery
});
this.add('PiwikAnalytics_cookieDomain', false, {
type: 'boolean',
public: true,
i18nLabel: 'PiwikAnalytics_cookieDomain',
enableQuery
});
this.add('PiwikAnalytics_domains', '', {
type: 'string',
multiline: true,
public: true,
i18nLabel: 'PiwikAnalytics_domains',
enableQuery
});
});
this.section('Analytics_Google', function() {

@ -332,13 +332,22 @@ RocketChat.API.v1.addRoute('channels.leave', { authRequired: true }, {
RocketChat.API.v1.addRoute('channels.list', { authRequired: true }, {
get: {
//This is like this only to provide an example of how we routes can be defined :X
//This is defined as such only to provide an example of how the routes can be defined :X
action() {
const { offset, count } = this.getPaginationItems();
const { sort, fields, query } = this.parseJsonQuery();
const ourQuery = Object.assign({}, query, { t: 'c' });
//Special check for the permissions
if (RocketChat.authz.hasPermission(this.userId, 'view-joined-room')) {
ourQuery.usernames = {
$in: [ this.user.username ]
};
} else if (!RocketChat.authz.hasPermission(this.userId, 'view-c-room')) {
return RocketChat.API.v1.unauthorized();
}
const rooms = RocketChat.models.Rooms.find(ourQuery, {
sort: sort ? sort : { name: 1 },
skip: offset,

@ -19,12 +19,17 @@ RocketChat.API.v1.addRoute('users.create', { authRequired: true }, {
this.bodyParams.joinDefaultChannels = true;
}
if (this.bodyParams.customFields) {
RocketChat.validateCustomFields(this.bodyParams.customFields);
}
const newUserId = RocketChat.saveUser(this.userId, this.bodyParams);
if (this.bodyParams.customFields) {
RocketChat.saveCustomFields(newUserId, this.bodyParams.customFields);
RocketChat.saveCustomFieldsWithoutValidation(newUserId, this.bodyParams.customFields);
}
if (typeof this.bodyParams.active !== 'undefined') {
Meteor.runAsUser(this.userId, () => {
Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active);
@ -105,6 +110,10 @@ RocketChat.API.v1.addRoute('users.info', { authRequired: true }, {
RocketChat.API.v1.addRoute('users.list', { authRequired: true }, {
get() {
if (!RocketChat.authz.hasPermission(this.userId, 'view-d-room')) {
return RocketChat.API.v1.unauthorized();
}
const { offset, count } = this.getPaginationItems();
const { sort, fields, query } = this.parseJsonQuery();

@ -0,0 +1,14 @@
/* globals RoomRoles */
Meteor.startup(function() {
RocketChat.Notifications.onLogged('Users:NameChanged', function({_id, name}) {
RoomRoles.update({
'u._id': _id
}, {
$set: {
'u.name': name
}
}, {
multi: true
});
});
});

@ -34,6 +34,7 @@ Package.onUse(function(api) {
api.addFiles('client/requiresPermission.html', ['client']);
api.addFiles('client/route.js', ['client']);
api.addFiles('client/usersNameChanged.js', ['client']);
// views
api.addFiles('client/views/permissions.html', ['client']);

@ -0,0 +1,6 @@
/* globals emojione */
Meteor.startup(function() {
RocketChat.callbacks.add('beforeNotifyUser', (message) => {
return emojione.shortnameToUnicode(message);
});
});

@ -13,9 +13,10 @@ Package.onUse(function(api) {
'rocketchat:lib'
]);
api.addFiles('emojiPicker.js', 'client');
api.addFiles('emojiPicker.js');
api.addFiles('rocketchat.js', 'client');
api.addFiles('rocketchat.js');
api.addFiles('sprites.css', 'client');
api.addFiles('callbacks.js', 'server');
});

@ -16,7 +16,7 @@ Package.onUse(function(api) {
]);
api.addFiles('function-isSet.js', 'client');
api.addFiles('rocketchat.js', 'client');
api.addFiles('rocketchat.js');
api.addFiles('emojiParser.js', 'client');

@ -54,7 +54,8 @@ Meteor.methods({
msg: '',
file: {
_id: file._id,
name: file.name
name: file.name,
type: file.type
},
groupable: false,
attachments: [attachment]

@ -991,6 +991,14 @@
"Pinned_a_message": "hat eine Nachricht angeheftet:",
"Pinned_Messages": "Fixierte Nachrichten",
"PiwikAnalytics_siteId_Description": "Die Website-ID zur Identifizierung dieser Website. Beispiel: 17",
"PiwikAdditionalTrackers" : "Zusätzliche Piwik Websites",
"PiwikAdditionalTrackers_Description" : "Geben Sie hier weitere Piwik Website URLs und SiteIDs in folgendem Format an, wenn Sie dieselben Daten in verschiedene Piwik Instanzen tracken möchten: [ { \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 }, { \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 } ]",
"PiwikAnalytics_prependDomain" : "Domain voranstellen",
"PiwikAnalytics_prependDomain_Description" : "Domain der Seite beim Tracken dem Seitentitel voranstellen",
"PiwikAnalytics_cookieDomain" : "Alle Subdomains",
"PiwikAnalytics_cookieDomain_Description" : "Besucher aufzeichnen auf allen Subdomains",
"PiwikAnalytics_domains" : "Verberge ausgehende Links",
"PiwikAnalytics_domains_Description" : "Verberge im Bericht über 'ausgehende Verweise' alle Klicks auf bekannte Alias-URLs. Bitte tragen Sie pro Zeile einen Domainnamen ein, verwenden Sie dabei keine Trennzeichen.",
"PiwikAnalytics_url_Description": "Die Piwik URL benötigt ein trailing slash. Beispiel: //piwik.rocket.chat/",
"Placeholder_for_email_or_username_login_field": "Platzhalter für das Feld der E-Mail-Adresse und des Benutzernamen",
"Placeholder_for_password_login_field": "Platzhalter für das Feld des Anmeldepassworts",

@ -133,6 +133,7 @@
"Add_custom_oauth": "Add custom oauth",
"Add_Domain": "Add Domain",
"Add_manager": "Add manager",
"Add_Role": "Add Role",
"Add_user": "Add user",
"Add_User": "Add User",
"Add_users": "Add users",
@ -178,6 +179,7 @@
"API_Drupal_URL": "Drupal Server URL",
"API_Drupal_URL_Description": "Example: https://domain.com (excluding trailing slash)",
"API_Embed": "Embed Link Previews",
"API_Embed_UserAgent": "Embed request user agent",
"API_Embed_Description": "Whether embedded link previews are enabled or not when a user posts a link to a website.",
"API_EmbedCacheExpirationDays": "Embed cache expiration days",
"API_EmbedDisabledFor": "Disable Embed for Users",
@ -319,6 +321,7 @@
"Chat_button": "Chat button",
"Chat_closed": "Chat closed",
"Chat_closed_successfully": "Chat closed successfully",
"Chat_Now": "Chat Now",
"Chat_window": "Chat window",
"Chatops_Enabled": "Enable Chatops",
"Chatops_Title": "Chatops Panel",
@ -333,6 +336,8 @@
"Clear_all_unreads_question": "Clear all unreads?",
"Click_here": "Click here",
"Click_here_for_more_info": "Click here for more info",
"UI_Click_Direct_Message": "Click to Create Direct Message",
"UI_Click_Direct_Message_Description": "Skip opening profile tab, instead go straight to conversation",
"Client_ID": "Client ID",
"Client_Secret": "Client Secret",
"Clients_will_refresh_in_a_few_seconds": "Clients will refresh in a few seconds",
@ -421,6 +426,7 @@
"Desktop_Notifications_Duration": "Desktop Notifications Duration",
"Desktop_Notifications_Duration_Description": "Seconds to display desktop notification. This may affect OS X Notification Center. Enter 0 to use default browser settings and not affect OS X Notification Center.",
"Desktop_Notifications_Enabled": "Desktop Notifications are Enabled",
"Different_Style_For_User_Mentions": "Different style for user mentions",
"Direct_message_someone": "Direct message someone",
"Direct_Messages": "Direct Messages",
"Disable_Notifications": "Disable Notifications",
@ -1107,6 +1113,7 @@
"Notification_Duration": "Notification Duration",
"Notifications": "Notifications",
"Notifications_Muted_Description": "If you choose to mute everything, you won't see the room highlight in the list when there are new messages, except for mentions. Muting notifications will override notifications settings.",
"Notifications_Sound_Volume": "Notifications sound volume",
"Notify_all_in_this_room": "Notify all in this room",
"Notify_active_in_this_room": "Notify active users in this room",
"Num_Agents": "# Agents",
@ -1319,6 +1326,14 @@
"Pinned_a_message": "Pinned a message:",
"Pinned_Messages": "Pinned Messages",
"PiwikAnalytics_siteId_Description": "The site id to use for identifying this site. Example: 17",
"PiwikAdditionalTrackers" : "Additional piwik sites",
"PiwikAdditionalTrackers_Description" : "Enter addtitional Piwik website URLs and SiteIDs in the following format, if you wnat to track the same data into different websites: [ { \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 }, { \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 } ]",
"PiwikAnalytics_prependDomain" : "Prepend domain",
"PiwikAnalytics_prependDomain_Description" : "Prepend the site domain to the page title when tracking",
"PiwikAnalytics_cookieDomain" : "All subdomains",
"PiwikAnalytics_cookieDomain_Description" : "Track visitors across all subdomains",
"PiwikAnalytics_domains" : "Hide outgoing links",
"PiwikAnalytics_domains_Description" : "In the 'Outlinks' report, hide clicks to known alias URLs. Please insert one domain per line and do not use any separators.",
"PiwikAnalytics_url_Description": "The url where the Piwik resides, be sure to include the trailing slash. Example: //piwik.rocket.chat/",
"Placeholder_for_email_or_username_login_field": "Placeholder for email or username login field",
"Placeholder_for_password_login_field": "Placeholder for password login field",
@ -1459,6 +1474,7 @@
"Rooms": "Rooms",
"Running_Instances": "Running Instances",
"S_new_messages_since_s": "%s new messages since %s",
"Same_Style_For_Mentions": "Same style for mentions",
"SAML": "SAML",
"SAML_Custom_Cert": "Custom Certificate",
"SAML_Custom_Entry_point": "Custom Entry Point",
@ -1492,6 +1508,7 @@
"Select_an_avatar": "Select an avatar",
"Select_file": "Select file",
"Select_service_to_login": "Select a service to login to load your picture or upload one directly from your computer",
"Select_role": "Select a Role",
"Select_user": "Select user",
"Select_users": "Select users",
"Selected_agents": "Selected agents",
@ -1721,6 +1738,7 @@
"Type_your_new_password": "Type your new password",
"UI_DisplayRoles": "Display Roles",
"UI_Merge_Channels_Groups": "Merge private groups with channels",
"UI_Unread_Counter_Style": "Unread counter style",
"UI_Use_Name_Avatar": "Use full name initials to generate default avatar",
"UI_Use_Real_Name": "Use Real Name",
"Unarchive": "Unarchive",
@ -1795,6 +1813,8 @@
"User_unmuted_by": "User <em>__user_unmuted__</em> unmuted by <em>__user_by__</em>.",
"User_unmuted_in_room": "User unmuted in room",
"User_updated_successfully": "User updated successfully",
"User_uploaded_file": "Uploaded a file",
"User_uploaded_image": "Uploaded an image",
"Username": "Username",
"Username_and_message_must_not_be_empty": "Username and message must not be empty.",
"Username_cant_be_empty": "The username cannot be empty",

@ -872,6 +872,7 @@
"Nothing_found": "No se encontró nada",
"Notification_Duration": "Duración de la notificación",
"Notifications": "Notificaciones",
"Notifications_Sound_Volume": "Volumen del sonido de las notificaciones",
"Notify_all_in_this_room": "Notificar a todos en este canal",
"Num_Agents": "# de Agentes",
"Number_of_messages": "Número de mensajes",
@ -1362,4 +1363,4 @@
"Your_mail_was_sent_to_s": "Su correo electrónico fue enviado a %s",
"Your_password_is_wrong": "Su contraseña es incorrecta!",
"Your_push_was_sent_to_s_devices": "Su push fue enviado a los dispositivos %s"
}
}

@ -817,6 +817,7 @@
"Nothing": "Nada",
"Nothing_found": "Nada encontrado",
"Notifications": "Notificações",
"Notifications_Sound_Volume": "Volume do som de notificações",
"Notify_all_in_this_room": "Notificar todos nesta sala",
"Num_Agents": "# Agentes",
"Number_of_messages": "Número de mensagens",

@ -748,6 +748,7 @@
"Nothing": "asgjë",
"Nothing_found": "Asgjë për të gjetur",
"Notifications": "Njoftime",
"Notifications_Sound_Volume": "Vëllimi i tingullit të njoftimeve",
"Notify_all_in_this_room": "Njoftojë të gjithë në këtë dhomë",
"Num_Agents": "# Agents",
"Number_of_messages": "Numri i mesazheve",
@ -1219,4 +1220,4 @@
"Your_mail_was_sent_to_s": "maili juaj u dërgua në %s",
"Your_password_is_wrong": "Fjalëkalimi juaj është e gabuar!",
"Your_push_was_sent_to_s_devices": "shtytje juaj u dërgua në pajisjet %s"
}
}

@ -165,8 +165,8 @@ RocketChat.integrations.triggerHandler = new class RocketChatIntegrationHandler
}
let tmpRoom;
if (nameOrId || trigger.targetRoom) {
tmpRoom = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: nameOrId || trigger.targetRoom, errorOnEmpty: false }) || room;
if (nameOrId || trigger.targetRoom || message.channel) {
tmpRoom = RocketChat.getRoomByNameOrIdWithOptionToJoin({ currentUserId: user._id, nameOrId: nameOrId || message.channel || trigger.targetRoom, errorOnEmpty: false }) || room;
} else {
tmpRoom = room;
}

@ -79,8 +79,8 @@ RocketChat.Notifications = new class {
unRoom(room, eventName, callback) {
return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback);
}
unUser(callback) {
return this.streamUser.removeListener(Meteor.userId(), callback);
unUser(eventName, callback) {
return this.streamUser.removeListener(`${ Meteor.userId() }/${ eventName }`, callback);
}
};

@ -1,5 +1,10 @@
/*globals currentTracker */
this.roomExit = function() {
// 7370 - Close flex-tab when opening a room on mobile UI
if (window.matchMedia('(max-width: 500px)').matches) {
const templateData = Blaze.getData(document.querySelector('.flex-tab'));
templateData && templateData.tabBar && templateData.tabBar.close();
}
RocketChat.callbacks.run('roomExit');
BlazeLayout.render('main', {
center: 'none'

@ -82,6 +82,7 @@ Package.onUse(function(api) {
api.addFiles('server/functions/removeUserFromRoom.js', 'server');
api.addFiles('server/functions/saveUser.js', 'server');
api.addFiles('server/functions/saveCustomFields.js', 'server');
api.addFiles('server/functions/saveCustomFieldsWithoutValidation.js', 'server');
api.addFiles('server/functions/sendMessage.js', 'server');
api.addFiles('server/functions/settings.js', 'server');
api.addFiles('server/functions/setUserAvatar.js', 'server');
@ -90,6 +91,7 @@ Package.onUse(function(api) {
api.addFiles('server/functions/setEmail.js', 'server');
api.addFiles('server/functions/unarchiveRoom.js', 'server');
api.addFiles('server/functions/updateMessage.js', 'server');
api.addFiles('server/functions/validateCustomFields.js', 'server');
api.addFiles('server/functions/Notifications.js', 'server');
// SERVER LIB

@ -1,3 +1,3 @@
{
"version": "0.57.0-develop"
"version": "0.58.0-develop"
}

@ -1,56 +1,6 @@
RocketChat.saveCustomFields = function(userId, formData) {
if (s.trim(RocketChat.settings.get('Accounts_CustomFields')) !== '') {
let customFieldsMeta;
try {
customFieldsMeta = JSON.parse(RocketChat.settings.get('Accounts_CustomFields'));
} catch (e) {
throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields');
}
const customFields = {};
Object.keys(customFieldsMeta).forEach((fieldName) => {
const field = customFieldsMeta[fieldName];
customFields[fieldName] = formData[fieldName];
if (field.required && !formData[fieldName]) {
throw new Meteor.Error('error-user-registration-custom-field', `Field ${ fieldName } is required`, { method: 'registerUser' });
}
if (field.type === 'select' && field.options.indexOf(formData[fieldName]) === -1) {
throw new Meteor.Error('error-user-registration-custom-field', `Value for field ${ fieldName } is invalid`, { method: 'registerUser' });
}
if (field.maxLength && formData[fieldName].length > field.maxLength) {
throw new Meteor.Error('error-user-registration-custom-field', `Max length of field ${ fieldName } ${ field.maxLength }`, { method: 'registerUser' });
}
if (field.minLength && formData[fieldName].length < field.minLength) {
throw new Meteor.Error('error-user-registration-custom-field', `Min length of field ${ fieldName } ${ field.minLength }`, { method: 'registerUser' });
}
});
// for fieldName, field of customFieldsMeta
RocketChat.models.Users.setCustomFields(userId, customFields);
Object.keys(customFields).forEach((fieldName) => {
if (!customFieldsMeta[fieldName].modifyRecordField) {
return;
}
const modifyRecordField = customFieldsMeta[fieldName].modifyRecordField;
const update = {};
if (modifyRecordField.array) {
update.$addToSet = {};
update.$addToSet[modifyRecordField.field] = customFields[fieldName];
} else {
update.$set = {};
update.$set[modifyRecordField.field] = customFields[fieldName];
}
RocketChat.models.Users.update(userId, update);
});
return true;
RocketChat.validateCustomFields(formData);
return RocketChat.saveCustomFieldsWithoutValidation(userId, formData);
}
};

@ -0,0 +1,33 @@
RocketChat.saveCustomFieldsWithoutValidation = function(userId, formData) {
if (s.trim(RocketChat.settings.get('Accounts_CustomFields')) !== '') {
let customFieldsMeta;
try {
customFieldsMeta = JSON.parse(RocketChat.settings.get('Accounts_CustomFields'));
} catch (e) {
throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields');
}
const customFields = formData;
// for fieldName, field of customFieldsMeta
RocketChat.models.Users.setCustomFields(userId, customFields);
Object.keys(customFields).forEach((fieldName) => {
if (!customFieldsMeta[fieldName].modifyRecordField) {
return;
}
const modifyRecordField = customFieldsMeta[fieldName].modifyRecordField;
const update = {};
if (modifyRecordField.array) {
update.$addToSet = {};
update.$addToSet[modifyRecordField.field] = customFields[fieldName];
} else {
update.$set = {};
update.$set[modifyRecordField.field] = customFields[fieldName];
}
RocketChat.models.Users.update(userId, update);
});
}
};

@ -19,7 +19,8 @@ RocketChat.sendMessage = function(user, message, room, upsert = false) {
}
}
if (message.parseUrls !== false) {
const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g);
const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\(\)\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g);
if (urls) {
message.urls = urls.map(function(url) {
return {
@ -53,7 +54,7 @@ RocketChat.sendMessage = function(user, message, room, upsert = false) {
Meteor.defer(() => {
// Execute all callbacks
message.sandstormSessionId = sandstormSessionId;
return RocketChat.callbacks.run('afterSaveMessage', message, room);
return RocketChat.callbacks.run('afterSaveMessage', message, room, user._id);
});
return message;
};

@ -25,6 +25,6 @@ RocketChat.updateMessage = function(message, user) {
const room = RocketChat.models.Rooms.findOneById(message.rid);
Meteor.defer(function() {
RocketChat.callbacks.run('afterSaveMessage', RocketChat.models.Messages.findOneById(tempid), room);
RocketChat.callbacks.run('afterSaveMessage', RocketChat.models.Messages.findOneById(tempid), room, user._id);
});
};

@ -0,0 +1,39 @@
RocketChat.validateCustomFields = function(fields) {
// Special Case:
// If an admin didn't set any custom fields there's nothing to validate against so consider any customFields valid
if (s.trim(RocketChat.settings.get('Accounts_CustomFields')) === '') {
return;
}
let customFieldsMeta;
try {
customFieldsMeta = JSON.parse(RocketChat.settings.get('Accounts_CustomFields'));
} catch (e) {
throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields');
}
const customFields = {};
Object.keys(customFieldsMeta).forEach((fieldName) => {
const field = customFieldsMeta[fieldName];
customFields[fieldName] = fields[fieldName];
const fieldValue = s.trim(fields[fieldName]);
if (field.required && fieldValue === '') {
throw new Meteor.Error('error-user-registration-custom-field', `Field ${ fieldName } is required`, {method: 'registerUser'});
}
if (field.type === 'select' && field.options.indexOf(fields[fieldName]) === -1) {
throw new Meteor.Error('error-user-registration-custom-field', `Value for field ${ fieldName } is invalid`, {method: 'registerUser'});
}
if (field.maxLength && fieldValue.length > field.maxLength) {
throw new Meteor.Error('error-user-registration-custom-field', `Max length of field ${ fieldName } ${ field.maxLength }`, {method: 'registerUser'});
}
if (field.minLength && fieldValue.length < field.minLength) {
throw new Meteor.Error('error-user-registration-custom-field', `Min length of field ${ fieldName } ${ field.minLength }`, {method: 'registerUser'});
}
});
};

@ -22,6 +22,22 @@ function replaceMentionedUsernamesWithFullNames(message, mentions) {
return message;
}
/**
* This function returns a string ready to be shown in the notification
*
* @param {object} message the message to be parsed
*/
function parseMessageText(message, userId) {
const user = RocketChat.models.Users.findOneById(userId);
const lng = user && user.language || RocketChat.settings.get('language') || 'en';
if (!message.msg && message.attachments[0]) {
message.msg = message.attachments[0].image_type ? TAPi18n.__('User_uploaded_image', {lng}) : TAPi18n.__('User_uploaded_file', {lng});
}
message.msg = RocketChat.callbacks.run('beforeNotifyUser', message.msg);
return message.msg;
}
/**
* Send notification to user
*
@ -31,7 +47,10 @@ function replaceMentionedUsernamesWithFullNames(message, mentions) {
* @param {number} duration Duration of notification
*/
function notifyUser(userId, user, message, room, duration) {
const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true;
message.msg = parseMessageText(message, userId);
if (UI_Use_Real_Name) {
message.msg = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions);
}
@ -84,7 +103,8 @@ function getBadgeCount(userId) {
}, 0);
}
RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
RocketChat.callbacks.add('afterSaveMessage', function(message, room, userId) {
// skips this callback if the message was edited
if (message.editedAt) {
return message;
@ -159,7 +179,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
let push_message;
//Set variables depending on Push Notification settings
if (RocketChat.settings.get('Push_show_message')) {
push_message = message.msg;
push_message = parseMessageText(message, userId);
} else {
push_message = ' ';
}

@ -1,6 +1,5 @@
Meteor.methods({
getRoomRoles(rid) {
check(rid, String);
if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousRead') === false) {
@ -20,7 +19,19 @@ Meteor.methods({
}
};
const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true;
const roles = RocketChat.models.Roles.find({ scope: 'Subscriptions', description: { $exists: 1, $ne: '' } }).fetch();
return RocketChat.models.Subscriptions.findByRoomIdAndRoles(rid, _.pluck(roles, '_id'), options).fetch();
const subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndRoles(rid, _.pluck(roles, '_id'), options).fetch();
if (!UI_Use_Real_Name) {
return subscriptions;
} else {
return subscriptions.map(subscription => {
const user = RocketChat.models.Users.findOneById(subscription.u._id);
subscription.u.name = user && user.name;
return subscription;
});
}
}
});

@ -148,6 +148,9 @@ class ModelUsers extends RocketChat.models._Base {
},
{
name: termRegex
},
{
'emails.address': termRegex
}
]
},

@ -789,6 +789,10 @@ RocketChat.settings.addGroup('Message', function() {
type: 'boolean',
'public': true
});
this.add('API_Embed_UserAgent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', {
type: 'string',
'public': true
});
this.add('API_EmbedCacheExpirationDays', 30, {
type: 'int',
'public': false
@ -1036,6 +1040,23 @@ RocketChat.settings.addGroup('Layout', function() {
type: 'boolean',
'public': true
});
this.add('UI_Click_Direct_Message', false, {
type: 'boolean',
'public': true
});
this.add('UI_Unread_Counter_Style', 'Different_Style_For_User_Mentions', {
type: 'select',
values: [
{
key: 'Same_Style_For_Mentions',
i18nLabel: 'Same_Style_For_Mentions'
}, {
key: 'Different_Style_For_User_Mentions',
i18nLabel: 'Different_Style_For_User_Mentions'
}
],
'public': true
});
});
});

@ -58,7 +58,10 @@ this.visitor = new class {
// notification sound
if (Session.equals('sound', true) && msg.u._id !== Meteor.userId()) {
$('#chatAudioNotification')[0].play();
const audioVolume = Meteor.user() && Meteor.user().settings && Meteor.user().settings.preferences && Meteor.user().settings.preferences.notificationsSoundVolume || 100;
const audio = document.getElementById('chatAudioNotification');
audio.volume = Number((audioVolume/100).toPrecision(2));
audio.play();
}
}
});

@ -72,8 +72,8 @@ this.Notifications = new class {
unRoom(room, eventName, callback) {
return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback);
}
unUser(callback) {
return this.streamUser.removeListener(Meteor.userId(), callback);
unUser(eventName, callback) {
return this.streamUser.removeListener(`${ Meteor.userId() }/${ eventName }`, callback);
}
};

@ -127,7 +127,7 @@ RocketChat.Livechat = {
if (this.connection) {
userData.userAgent = this.connection.httpHeaders['user-agent'];
userData.ip = this.connection.httpHeaders['x-real-ip'] || this.connection.clientAddress;
userData.ip = this.connection.httpHeaders['x-real-ip'] || this.connection.httpHeaders['x-forwarded-for'] || this.connection.clientAddress;
userData.host = this.connection.httpHeaders.host;
}

@ -7,6 +7,9 @@ Meteor.startup(function() {
action() {
const message = this._arguments[1];
RocketChat.MessageAction.hideDropDown();
if (window.matchMedia('(max-width: 500px)').matches) {
Template.instance().tabBar.close();
}
return RoomHistoryManager.getSurroundingMessages(message, 50);
},
validation(message) {

@ -44,7 +44,6 @@ export default class {
return match;
}
const name = this.useRealName && mentionObj && mentionObj.name;
console.log({name});
return `<a class="mention-link ${ username === me ? 'mention-link-me background-primary-action-color':'' }" data-username="${ username }" title="${ name ? username : '' }">${ name || match }</a>`;
});

@ -62,6 +62,9 @@ Meteor.startup(function() {
action() {
const message = this._arguments[1];
RocketChat.MessageAction.hideDropDown();
if (window.matchMedia('(max-width: 500px)').matches) {
Template.instance().tabBar.close();
}
return RoomHistoryManager.getSurroundingMessages(message, 50);
},
validation(message) {

@ -54,6 +54,9 @@ Meteor.startup(function() {
action() {
const message = this._arguments[1];
RocketChat.MessageAction.hideDropDown();
if (window.matchMedia('(max-width: 500px)').matches) {
Template.instance().tabBar.close();
}
return RoomHistoryManager.getSurroundingMessages(message, 50);
},
validation(message) {

@ -88,7 +88,7 @@ const getUrlContent = function(urlObj, redirectCount = 5, callback) {
gzip: true,
maxRedirects: redirectCount,
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'
'User-Agent': RocketChat.settings.get('API_Embed_UserAgent')
}
};
let headers = null;

@ -255,29 +255,37 @@ Template.pushNotificationsFlexTab.events({
'click [data-play]'(e) {
e.preventDefault();
let audio = $(e.currentTarget).data('play');
const user = Meteor.user();
if (!audio || audio === 'none') {
audio = user && user.settings && user.settings.preferences && user.settings.preferences.newMessageNotification || 'chime';
}
if (audio && audio !== 'none') {
const audioVolume = user && user.settings && user.settings.preferences && user.settings.preferences.notificationsSoundVolume || 100;
const $audio = $(`audio#${ audio }`);
if ($audio && $audio[0] && $audio[0].play) {
$audio[0].volume = Number((audioVolume/100).toPrecision(2));
$audio[0].play();
}
} else {
audio = Meteor.user() && Meteor.user().settings && Meteor.user().settings.preferences && Meteor.user().settings.preferences.newMessageNotification || 'chime';
if (audio && audio !== 'none') {
const $audio = $(`audio#${ audio }`);
if ($audio && $audio[0] && $audio[0].play) {
$audio[0].play();
}
}
}
},
'change select[name=audioNotification]'(e) {
e.preventDefault();
const audio = $(e.currentTarget).val();
const user = Meteor.user();
if (audio && audio !== 'none') {
const audioVolume = user && user.settings && user.settings.preferences && user.settings.preferences.notificationsSoundVolume || 100;
const $audio = $(`audio#${ audio }`);
if ($audio && $audio[0] && $audio[0].play) {
$audio[0].volume = Number((audioVolume/100).toPrecision(2));
$audio[0].play();
}
}

@ -0,0 +1,16 @@
Package.describe({
name: 'rocketchat:slider',
version: '0.0.1',
summary: 'UI slider component for input range.',
git: '',
documentation: 'README.md'
});
Package.onUse(function(api) {
api.use('ecmascript');
api.use('templating', 'client');
api.use('rocketchat:theme');
api.addFiles('rocketchat-slider.html', 'client');
api.addFiles('rocketchat-slider.js', 'client');
});

@ -0,0 +1,6 @@
<template name="slider">
<div class="range-slider">
<input class="range-slider-range tertiary-background-color" type="range" id="{{id}}" value="{{value}}" min="{{min}}" max="{{max}}">
<span class="range-slider-value" id="{{id}}_value"></span>
</div>
</template>

@ -0,0 +1,17 @@
Template.slider.onRendered(function() {
const params = this.data;
const rangeSlider = function() {
const range = $(`#${ params.id }`);
const labelValue = $(`#${ params.id }_value`);
labelValue.html(params.value);
range.on('input', function() {
labelValue.html(this.value);
});
};
rangeSlider();
});

@ -0,0 +1,18 @@
.chip-container {
list-style-type: none;
margin: 15px;
}
.chip-container li {
display: inline-block;
background-color: #dddddd;
border-radius: 10px;
padding: 2px 8px 2px 2px;
margin: 1px 0;
cursor: pointer;
}
.chip-container li .icon-plus-circled {
opacity: 0.5;
font-size: 0.8rem;
}

@ -1245,7 +1245,7 @@
right: 6px;
font-size: 11px;
top: 6px;
line-height: 16px;
line-height: 14px;
font-weight: 800;
}
@ -2489,6 +2489,12 @@
&.selectable .message {
cursor: pointer;
}
&.has-leader {
& .wrapper {
padding-top: 57px;
}
}
}
.rc-old .ticks-bar {
@ -3447,6 +3453,10 @@ body:not(.is-cordova) {
& #password {
width: 70%;
}
& #roleSelect {
width: 70%;
}
}
& nav {
@ -4855,52 +4865,38 @@ body:not(.is-cordova) {
color: #555555;
}
.room-leader-container {
height: 54px;
}
.room-leader {
position: fixed;
top: 60px;
left: 0;
z-index: 1;
background: #ffffff;
width: calc(100% - 40px);
color: #555555;
border-bottom: solid 1px #eaeaea;
height: 54px;
}
.room-leader .thumb {
top: 6px;
}
.room-leader .right {
position: absolute;
top: 10px;
left: 70px;
}
height: 57px;
left: 0;
right: 0;
z-index: 2;
border-bottom: 1px solid;
padding-bottom: 8px;
transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1), visibility 0.15s cubic-bezier(0.5, 0, 0.1, 1);
visibility: visible;
.leader-status .status-text {
text-transform: capitalize;
padding-left: 15px;
font-size: 14px;
}
&.animated-hidden {
transform: translateY(-100%);
visibility: hidden;
}
.leader-status .color-ball {
width: 10px;
height: 10px;
position: absolute;
border-radius: 5px;
margin-top: 5px;
background: grey;
}
& .leader-name {
font-size: 18px;
}
.leader-status .color-ball.online {
background: green;
}
& .leader-status {
& .status-text {
padding-left: 15px;
font-size: 14px;
}
.leader-info .leader-name {
font-size: 18px;
& .color-ball {
width: 10px;
height: 10px;
position: absolute;
border-radius: 5px;
margin-top: 5px;
}
}
}

@ -0,0 +1,61 @@
.range-slider {
margin: 0;
width: 100%;
}
.range-slider-range {
appearance: none;
width: calc(100% - 73px);
height: 10px;
border-radius: 5px;
outline: none;
padding: 0;
margin: 0;
}
.range-slider-range::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
transition: background 0.15s ease-in-out;
}
.range-slider-range::-moz-range-thumb {
width: 20px;
height: 20px;
border: 0;
border-radius: 50%;
cursor: pointer;
transition: background 0.15s ease-in-out;
}
.range-slider-value {
display: inline-block;
position: relative;
width: 60px;
line-height: 20px;
text-align: center;
border-radius: 3px;
padding: 5px 10px;
margin-left: 8px;
}
.range-slider-value::after {
position: absolute;
top: 8px;
left: -7px;
width: 0;
height: 0;
border-top: 7px solid;
border-right: 7px solid;
border-bottom: 7px solid;
content: '';
}
.range-slider-range::-moz-range-track,
.range-slider-range::-moz-focus-inner,
.range-slider-range::-moz-focus-outer {
border: 0;
}

@ -14,19 +14,21 @@
@import 'imports/components/tooltip.css';
@import 'imports/components/modal/full-modal.css';
@import 'imports/components/modal/create-channel.css';
@import 'imports/slider.css';
@keyframes fadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: none;
}
to {
opacity: 1;
transform: none;
}
}
.fadeInDown {
animation-name: fadeInDown;
animation-duration: .3s;
animation-name: fadeInDown;
animation-duration: 0.3s;
}

@ -655,6 +655,11 @@ input:-webkit-autofill {
}
.unread {
color: @success-color;
border: 1px solid @success-color;
}
.unread.unread-mention {
background-color: @success-color;
color: contrast(@success-color, #000000, #ffffff, 50%);
}
@ -751,6 +756,10 @@ i.status-online {
color: @status-online;
}
.status-bg-online {
background-color: @status-online;
}
.account-box .status-online .thumb::after,
.account-box .status.online::after,
.popup-user-status-online,
@ -769,6 +778,10 @@ i.status-away {
color: @status-away;
}
.status-bg-away {
background-color: @status-away;
}
.account-box .status-away .thumb::after,
.account-box .status.away::after,
.popup-user-status-away,
@ -782,6 +795,10 @@ i.status-busy {
color: @status-busy;
}
.status-bg-busy {
background-color: @status-busy;
}
.account-box .status-busy .thumb::after,
.account-box .status.busy::after,
.popup-user-status-busy,
@ -795,6 +812,10 @@ i.status-offline {
color: @status-offline;
}
.status-bg-offline {
background-color: @status-offline;
}
.popup-user-status-offline,
.status-offline::after,
.user-image.status-offline .avatar::after {
@ -900,3 +921,46 @@ label.required::after {
opacity: inherit;
}
}
/** ----------------------------------------------------------------------------
* Input Range Slider
*/
.range-slider-range::-webkit-slider-thumb {
background-color: @primary-background-color;
}
.range-slider-range::-webkit-slider-thumb:hover {
background-color: darken(@success-color, 30%);
}
.range-slider-range:active::-webkit-slider-thumb {
background-color: darken(@success-color, 10%);
}
.range-slider-range::-moz-range-thumb {
background-color: @primary-background-color;
}
.range-slider-range::-moz-range-thumb:hover {
background-color: darken(@success-color, 30%);
}
.range-slider-range:active::-moz-range-thumb {
background-color: darken(@success-color, 10%);
}
.range-slider-value {
color: lighten(@tertiary-background-color, 50%);
background-color: @primary-background-color;
}
.range-slider-value::after {
border-top-color: transparent;
border-right-color: @primary-background-color;
border-bottom-color: transparent;
}
.range-slider-range::-moz-range-track {
background-color: @tertiary-background-color;
}

@ -218,6 +218,12 @@
</select>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Notifications_Sound_Volume"}}</label>
<div>
{{> slider id="notificationsSoundVolume" min="0" max="100" value=notificationsSoundVolume}}
</div>
</div>
</div>
</div>
</fieldset>

@ -71,6 +71,10 @@ Template.accountPreferences.helpers({
},
showRoles() {
return RocketChat.settings.get('UI_DisplayRoles');
},
notificationsSoundVolume() {
const user = Meteor.user();
return user && user.settings && user.settings.preferences && user.settings.preferences.notificationsSoundVolume || 100;
}
});
@ -132,6 +136,8 @@ Template.accountPreferences.onCreated(function() {
}));
data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val();
data.unreadAlert = $('#unreadAlert').find('input:checked').val();
data.notificationsSoundVolume = parseInt($('#notificationsSoundVolume').val());
Meteor.call('saveUserPreferences', data, function(error, results) {
if (results) {
toastr.success(t('Preferences_saved'));

@ -37,10 +37,10 @@ Template.adminRooms.helpers({
if (this.t === 'c') {
return TAPi18n.__('Channel');
} else if (this.t === 'd') {
return TAPi18n.__('Direct Message');
return TAPi18n.__('Direct_Messages');
}
if (this.t === 'p') {
return TAPi18n.__('Private Group');
return TAPi18n.__('Private_Groups');
}
},
'default'() {

@ -9,6 +9,9 @@ Meteor.startup(function() {
action() {
const message = this._arguments[1];
RocketChat.MessageAction.hideDropDown();
if (window.matchMedia('(max-width: 500px)').matches) {
Template.instance().tabBar.close();
}
return RoomHistoryManager.getSurroundingMessages(message, 50);
},
order: 100

@ -40,15 +40,28 @@
</label>
</div>
{{/if}}
{{#unless user}}
<div class="input-line">
<label for="role">{{_ "Role"}}</label>
<select id="role">
{{#each role}}
<option value="{{_id}}" selected="{{selectUserRole}}">{{name}}</option>
<div class="input-line" data="{{this}}">
<label for="role">{{_ "Add_Role"}}</label>
{{#let roles=role}}
<select id="roleSelect" {{disabled role}}>
<option value="placeholder" disabled selected>{{_ "Select_role"}}</option>
{{#each roles}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
{{/let}}
<button id="addRole" class="button" {{disabled role}}>{{_ 'Add_Role'}}</button>
<fieldset>
<ul class="chip-container current-user-roles">
{{#each userRoles}}
<li class="remove-role" title="{{this}}"><i class="icon-cancel-circled"></i>{{this}}</li>
{{/each}}
</ul>
</fieldset>
</div>
{{#unless user}}
<div class="input-line">
<label for="joinDefaultChannels">
<input type="checkbox" id="joinDefaultChannels" value="1" checked="true">

@ -1,6 +1,10 @@
import toastr from 'toastr';
Template.userEdit.helpers({
disabled(cursor) {
return cursor.count() === 0 ? 'disabled' : '';
},
canEditOrAdd() {
return (Template.instance().user && RocketChat.authz.hasAtLeastOnePermission('edit-other-user-info')) || (!Template.instance().user && RocketChat.authz.hasAtLeastOnePermission('create-user'));
},
@ -14,13 +18,12 @@ Template.userEdit.helpers({
},
role() {
return RocketChat.models.Roles.find({}, { sort: { description: 1, _id: 1 } });
const roles = Template.instance().roles.get();
return RocketChat.models.Roles.find({_id: {$nin:roles}, scope: 'Users'}, { sort: { description: 1, _id: 1 } });
},
selectUserRole() {
if (this._id === 'user') {
return 'selected';
}
userRoles() {
return Template.instance().roles.get();
},
name() {
@ -32,26 +35,50 @@ Template.userEdit.events({
'click .cancel'(e, t) {
e.stopPropagation();
e.preventDefault();
return t.cancel(t.find('form'));
t.roles.set([]);
t.cancel(t.find('form'));
},
'click .remove-role'(e, t) {
e.stopPropagation();
e.preventDefault();
let roles = t.roles.get();
roles = roles.filter(el => el !== this.valueOf());
t.roles.set(roles);
$(`[title=${ this }]`).remove();
},
'click #randomPassword'(e) {
e.stopPropagation();
e.preventDefault();
return $('#password').val(Random.id());
$('#password').val(Random.id());
},
'submit form'(e, t) {
'click #addRole'(e, instance) {
e.stopPropagation();
e.preventDefault();
if ($('#roleSelect').find(':selected').is(':disabled')) {
return;
}
const userRoles = [...instance.roles.get()];
userRoles.push($('#roleSelect').val());
instance.roles.set(userRoles);
$('#roleSelect').val('placeholder');
},
return t.save(e.currentTarget);
'submit form'(e, t) {
e.stopPropagation();
e.preventDefault();
t.save(e.currentTarget);
}
});
Template.userEdit.onCreated(function() {
let userData;
this.user = this.data != null ? this.data.user : undefined;
this.roles = this.user ? new ReactiveVar(this.user.roles) : new ReactiveVar([]);
const { tabBar } = Template.currentData();
@ -75,7 +102,15 @@ Template.userEdit.onCreated(function() {
userData.requirePasswordChange = this.$('#changePassword:checked').length > 0;
userData.joinDefaultChannels = this.$('#joinDefaultChannels:checked').length > 0;
userData.sendWelcomeEmail = this.$('#sendWelcomeEmail:checked').length > 0;
if (this.$('#role').val()) { userData.roles = [this.$('#role').val()]; }
const roleSelect = this.$('.remove-role').toArray();
if (roleSelect.length > 0) {
const notSorted = roleSelect.map(role => {
return role.title;
});
//Remove duplicate strings from the array
userData.roles = notSorted.filter((el, index) => notSorted.indexOf(el) === index);
}
return userData;
};
@ -93,6 +128,10 @@ Template.userEdit.onCreated(function() {
errors.push('Email');
}
if (!userData.roles) {
errors.push('Roles');
}
for (const error of Array.from(errors)) {
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) }));
}

@ -2,6 +2,7 @@
<div class="message-popup-results">
{{#if emojiEnabled}}
{{> messagePopup popupEmojiConfig}}
{{> messagePopup popupReactionEmojiConfig}}
{{/if}}
{{> messagePopup popupChannelConfig}}
{{> messagePopup popupUserConfig}}

@ -269,6 +269,47 @@ Template.messagePopupConfig.helpers({
return [];
}
const regExp = new RegExp(`^${ RegExp.escape(key) }`, 'i');
return Object.keys(collection).map(key => {
const value = collection[key];
return {
_id: key,
data: value
};
})
.filter(obj => regExp.test(obj._id))
.slice(0, 10)
.sort(function(a, b) {
if (a._id < b._id) {
return -1;
}
if (a._id > b._id) {
return 1;
}
return 0;
});
}
};
}
},
popupReactionEmojiConfig() {
if (RocketChat.emoji != null) {
const self = this;
return {
title: t('Emoji'),
collection: RocketChat.emoji.list,
template: 'messagePopupEmoji',
trigger: '\\+',
prefix: '+',
suffix: ' ',
getInput: self.getInput,
getFilter(collection, filter) {
const key = `${ filter }`;
if (!RocketChat.emoji.packages.emojione || RocketChat.emoji.packages.emojione.asciiList[key] || filter.length < 2) {
return [];
}
const regExp = new RegExp(`^${ RegExp.escape(key) }`, 'i');
return Object.keys(collection).map(key => {
const value = collection[key];

@ -51,11 +51,12 @@ export const RoomHistoryManager = new class {
typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined);
}
return Meteor.call('loadHistory', rid, ts, limit, ls, function(err, result) {
Meteor.call('loadHistory', rid, ts, limit, ls, function(err, result) {
if (err) {
return;
}
let previousHeight;
const {messages = []} = result;
room.unreadNotLoaded.set(result.unreadNotLoaded);
room.firstUnread.set(result.firstUnread);
@ -64,7 +65,7 @@ export const RoomHistoryManager = new class {
previousHeight = wrapper.scrollHeight;
}
result.messages.forEach(item => {
messages.forEach(item => {
if (item.t !== 'command') {
const roles = [
(item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {},
@ -87,8 +88,8 @@ export const RoomHistoryManager = new class {
room.isLoading.set(false);
if (room.loaded == null) { room.loaded = 0; }
room.loaded += result.messages.length;
if (result.messages.length < limit) {
room.loaded += messages.length;
if (messages.length < limit) {
return room.hasMore.set(false);
}
});

@ -185,6 +185,16 @@ this.ChatMessages = class ChatMessages {
const msg = input.value;
const msgObject = { _id: Random.id(), rid, msg};
if (msg.slice(0, 2) === '+:') {
const reaction = msg.slice(1).trim();
if (RocketChat.emoji.list[reaction]) {
const lastMessage = ChatMessage.findOne({rid}, { fields: { ts: 1 }, sort: { ts: -1 }});
Meteor.call('setReaction', reaction, lastMessage._id);
input.value = '';
return;
}
}
// Run to allow local encryption, and maybe other client specific actions to be run before send
return RocketChat.promises.run('onClientBeforeSendMessage', msgObject).then(msgObject => {

@ -80,14 +80,23 @@ const KonchatNotification = {
if (!Session.equals(`user_${ Meteor.userId() }_status`, 'busy')) {
const user = Meteor.user();
const newMessageNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newMessageNotification || 'chime';
const audioVolume = user && user.settings && user.settings.preferences && user.settings.preferences.notificationsSoundVolume || 100;
const sub = ChatSubscription.findOne({ rid }, { fields: { audioNotification: 1 } });
if (sub && sub.audioNotification !== 'none') {
if (sub && sub.audioNotification) {
const [audio] = $(`audio#${ sub.audioNotification }`);
return audio && audio.play && audio.play();
if (audio && audio.play) {
audio.volume = Number((audioVolume/100).toPrecision(2));
return audio.play();
}
} else if (newMessageNotification !== 'none') {
const [audio] = $(`audio#${ newMessageNotification }`);
return audio && audio.play && audio.play();
if (audio && audio.play) {
audio.volume = Number((audioVolume/100).toPrecision(2));
return audio.play();
}
}
}
}
@ -116,24 +125,28 @@ const KonchatNotification = {
};
Tracker.autorun(function() {
let audio;
const user = Meteor.user();
const newRoomNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newRoomNotification || 'door';
const audioVolume = user && user.settings && user.settings.preferences && user.settings.preferences.notificationsSoundVolume || 100;
if ((Session.get('newRoomSound') || []).length > 0) {
Tracker.nonreactive(function() {
const user = RocketChat.models.Users.findOne({ _id: Meteor.userId() }, { fields: { 'settings.preferences.newRoomNotification': 1 } });
const newRoomNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newRoomNotification || 'door';
if (!Session.equals(`user_${ Meteor.userId() }_status`, 'busy') && newRoomNotification !== 'none') {
[audio] = $(`audio#${ newRoomNotification }`);
return audio && audio.play && audio.play();
const [audio] = $(`audio#${ newRoomNotification }`);
if (audio && audio.play) {
audio.volume = Number((audioVolume/100).toPrecision(2));
return audio.play();
}
}
});
} else {
if (!audio) {
const [room] = $(`audio#${ newRoomNotification }`);
if (!room) {
return;
}
if (audio.pause) {
audio.pause();
audio.currentTime = 0;
audio = null;
if (room.pause) {
room.pause();
return room.currentTime = 0;
}
}
});

@ -93,7 +93,7 @@
</div>
{{/each}}
</div>
<div class="messages-box {{#if selectable}}selectable{{/if}} {{viewMode}}">
<div class="messages-box {{#if selectable}}selectable{{/if}} {{viewMode}} {{hasLeader}}">
<div class="ticks-bar"></div>
<button class="new-message background-primary-action-color color-content-background-color not">
<i class="icon-down-big"></i>
@ -109,26 +109,21 @@
</div>
</div>
{{/unless}}
{{#with roomLeader}}
<div class="room-leader message color-primary-font-color content-background-color border-component-color {{hideLeaderHeader}}">
<button class="thumb user-card-message">
{{> avatar username=username }}
</button>
<div class="leader-name">{{name}}</div>
<div class="leader-status userStatus">
<span class="color-ball status-bg-{{status}}"></span>
<span class="status-text leader-status-text">{{_ statusDisplay}}</span>
</div>
<a class="chat-now" href="{{chatNowLink}}">{{_ "Chat_Now"}}</a>
</div>
{{/with}}
<div class="wrapper {{#if hasMoreNext}}has-more-next{{/if}} {{hideUsername}} {{hideAvatar}}">
<ul aria-live="polite">
{{#if roomLeader}}
<li class="message room-leader-container">
<div class="room-leader">
<button class="thumb user-card-message">
<div class="avatar"><div class="avatar-image" style="background-image:url(/avatar/{{roomLeader.username}}?_dc=undefined);"></div></div></button>
<div class="right">
<div class="leader-info">
<div class="leader-name">{{roomLeader.name}}</div>
<div class="leader-status userStatus">
<span class="color-ball leader-status {{roomLeader.status}}"></span>
<span class="status-text leader-status-text">{{roomLeader.status}}</span>
</div>
</div>
</div>
<a class="chat-now" href="/direct/{{roomLeader.username}}">Chat Now</a>
</div>
</li>
{{/if}}
{{#if canPreview}}
{{#if hasMore}}
<li class="load-more">

@ -10,7 +10,23 @@ const isSubscribed = _id => ChatSubscription.find({ rid: _id }).count() > 0;
const favoritesEnabled = () => RocketChat.settings.get('Favorite_Rooms');
const userCanDrop = _id => !RocketChat.roomTypes.readOnly(_id, Meteor.user());
const openProfileTab = (e, instance, data) => {
const roomData = Session.get(`roomData${ data.rid }`);
if (RocketChat.Layout.isEmbedded()) {
fireGlobalEvent('click-user-card-message', { username: data.u.username });
e.preventDefault();
e.stopPropagation();
return;
}
if (['c', 'p', 'd'].includes(roomData.t)) {
instance.setUserDetail(data.u.username);
}
instance.tabBar.setTemplate('membersList');
return instance.tabBar.open();
};
Template.room.helpers({
isTranslated() {
const sub = ChatSubscription.findOne({ rid: this._id }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } });
@ -98,15 +114,20 @@ Template.room.helpers({
},
roomLeader() {
const roles = RoomRoles.find({rid: this._id, roles: 'leader'}).fetch();
if (roles.length > 0) {
const u = roles[0].u;
if (u._id === Meteor.user()._id) { return null; }
const currUser = RocketChat.models.Users.find({ _id: u._id}).fetch();
u['status'] = currUser.length > 0 ? 'online' : 'offline';
return u;
const roles = RoomRoles.findOne({ rid: this._id, roles: 'leader', 'u._id': { $ne: Meteor.userId() } });
if (roles) {
const leader = RocketChat.models.Users.findOne({ _id: roles.u._id }, { fields: { status: 1 }}) || {};
return {
...roles.u,
name: RocketChat.settings.get('UI_Use_Real_Name') ? (roles.u.name || roles.u.username) : roles.u.username,
status: leader.status || 'offline',
statusDisplay: (status => status.charAt(0).toUpperCase() + status.slice(1))(leader.status || 'offline')
};
}
return null;
},
chatNowLink() {
return RocketChat.roomTypes.getRouteLink('d', { name: this.username });
},
roomName() {
@ -264,6 +285,14 @@ Template.room.helpers({
};
});
return { buttons };
},
hideLeaderHeader() {
return Template.instance().hideLeaderHeader.get() ? 'animated-hidden' : '';
},
hasLeader() {
if (RoomRoles.findOne({ rid: this._id, roles: 'leader', 'u._id': { $ne: Meteor.userId() } }, { fields: { _id: 1 } })) {
return 'has-leader';
}
}
});
@ -290,16 +319,6 @@ Template.room.events({
}
},
'scroll .messages-box .wrapper'() {
const $wrapper = $('.messages-box .wrapper');
if ($wrapper.scrollTop() < lastScrollTop) {
$('.room-leader').removeClass('hidden');
} else if ($wrapper.scrollTop() > $('.room-leader-container').height()) {
$('.room-leader').addClass('hidden');
}
lastScrollTop = $wrapper.scrollTop();
},
'touchstart .message'(e, t) {
const touches = e.originalEvent.touches;
if (touches && touches.length) {
@ -466,24 +485,33 @@ Template.room.events({
if (!Meteor.userId() || !this._arguments) {
return;
}
const roomData = Session.get(`roomData${ this._arguments[1].rid }`);
if (RocketChat.settings.get('UI_Click_Direct_Message')) {
return Meteor.call('createDirectMessage', this._arguments[1].u.username, (error, result) => {
if (error) {
if (error.isClientSafe) {
openProfileTab(e, instance, this._arguments[1]);
} else {
return handleError(error);
}
}
if (RocketChat.Layout.isEmbedded()) {
fireGlobalEvent('click-user-card-message', { username: this._arguments[1].u.username });
e.preventDefault();
e.stopPropagation();
return;
if ((result != null ? result.rid : undefined) != null) {
return FlowRouter.go('direct', { username: this._arguments[1].u.username }, FlowRouter.current().queryParams);
}
});
} else {
openProfileTab(e, instance, this._arguments[1]);
}
},
if (['c', 'p', 'd'].includes(roomData.t)) {
instance.setUserDetail(this._arguments[1].u.username);
'scroll .wrapper': _.throttle(function(e, t) {
if (e.target.scrollTop < lastScrollTop) {
t.hideLeaderHeader.set(false);
} else if (e.target.scrollTop > $('.room-leader').height()) {
t.hideLeaderHeader.set(true);
}
lastScrollTop = e.target.scrollTop;
instance.tabBar.setTemplate('membersList');
return instance.tabBar.open();
},
'scroll .wrapper': _.throttle(function(e) {
if (RoomHistoryManager.isLoading(this._id) === false && RoomHistoryManager.hasMore(this._id) === true || RoomHistoryManager.hasMoreNext(this._id) === true) {
if (RoomHistoryManager.hasMore(this._id) === true && e.target.scrollTop === 0) {
return RoomHistoryManager.getMore(this._id);
@ -673,6 +701,8 @@ Template.room.onCreated(function() {
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
this.hideLeaderHeader = new ReactiveVar(false);
this.resetSelection = enabled => {
this.selectable.set(enabled);
$('.messages-box .message.selected').removeClass('selected');

@ -109,10 +109,8 @@ Meteor.methods({
}
});
DDPRateLimiter.addRule({
type: 'method',
name: 'createDirectMessage',
connectionId() {
return true;
RocketChat.RateLimiter.limitMethod('createDirectMessage', 10, 60000, {
userId(userId) {
return !RocketChat.authz.hasPermission(userId, 'send-many-messages');
}
}, 10, 60000);
});

@ -0,0 +1,32 @@
Meteor.methods({
getRoomNameById(rid) {
check(rid, String);
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'getRoomNameById'
});
}
const room = RocketChat.models.Rooms.findOneById(rid);
if (room == null) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'getRoomNameById'
});
}
const user = Meteor.user();
if (user && user.username && room.usernames.indexOf(user.username) !== -1) {
return room.name;
}
if (room.t !== 'c' || RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') !== true) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'getRoomNameById'
});
}
return room.name;
}
});

@ -59,6 +59,10 @@ Meteor.methods({
preferences.unreadAlert = settings.unreadAlert === '1' ? true : false;
}
if (settings.notificationsSoundVolume) {
preferences.notificationsSoundVolume = settings.notificationsSoundVolume;
}
preferences.desktopNotificationDuration = settings.desktopNotificationDuration - 0;
preferences.viewMode = settings.viewMode || 0;
preferences.hideUsernames = settings.hideUsernames === '1';

@ -50,7 +50,7 @@ Meteor.methods({
Meteor.call('setEmail', settings.email);
}
// Should be the last chack to prevent error when trying to check password for users without password
// Should be the last check to prevent error when trying to check password for users without password
if ((settings.newPassword) && RocketChat.settings.get('Accounts_AllowPasswordChange') === true) {
if (!checkPassword(user, settings.typedPassword)) {
throw new Meteor.Error('error-invalid-password', 'Invalid password', {

@ -13,6 +13,7 @@ const options = {
jitsiTimeout: 1,
description: 1,
default: 1,
customFields: 1,
// @TODO create an API to register this fields based on room type
livechatData: 1,

@ -86,7 +86,8 @@ Meteor.startup(function() {
initials = initials.toUpperCase();
}
const svg = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" pointer-events=\"none\" width=\"50\" height=\"50\" style=\"width: 50px; height: 50px; background-color: ${ color };\">\n <text text-anchor=\"middle\" y=\"50%\" x=\"50%\" dy=\"0.36em\" pointer-events=\"auto\" fill=\"#ffffff\" font-family=\"Helvetica, Arial, Lucida Grande, sans-serif\" style=\"font-weight: 400; font-size: 28px;\">\n ${ initials }\n </text>\n</svg>`;
const svg = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" pointer-events=\"none\" width=\"50\" height=\"50\">\n<rect height="50" width="50" fill=\"${ color }\"/>\n<text text-anchor=\"middle\" y=\"50%\" x=\"50%\" dy=\"0.36em\" pointer-events=\"auto\" fill=\"#ffffff\" font-family=\"Helvetica, Arial, Lucida Grande, sans-serif\" font-weight="400" font-size="28">\n${ initials }\n</text>\n</svg>`;
res.write(svg);
res.end();

@ -0,0 +1,28 @@
import {getCredentials, request, api, credentials} from './api-data.js';
export const customFieldText = {
type: 'text',
required: true,
minLength: 2,
maxLength: 10
};
export function setCustomFields(customFields, done) {
getCredentials((error) => {
if (error) {
return done(error);
}
const stringified = customFields ? JSON.stringify(customFields) : '';
request.post(api('settings/Accounts_CustomFields'))
.set(credentials)
.send({ 'value': stringified })
.expect(200)
.end(done);
});
}
export function clearCustomFields(done = () => {}) {
setCustomFields(null, done);
}

@ -5,140 +5,236 @@
import {getCredentials, api, login, request, credentials, apiEmail, apiUsername, targetUser, log} from '../../data/api-data.js';
import {adminEmail, password} from '../../data/user.js';
import {imgURL} from '../../data/interactions.js';
import {customFieldText, clearCustomFields, setCustomFields} from '../../data/custom-fields.js';
describe('[Users]', function() {
this.retries(0);
before(done => getCredentials(done));
it('/users.create:', (done) => {
request.post(api('users.create'))
.set(credentials)
.send({
email: apiEmail,
name: apiUsername,
username: apiUsername,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified:true
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', apiUsername);
expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', apiUsername);
targetUser._id = res.body.user._id;
})
.end(done);
});
describe('[/users.create]', () => {
before(done => clearCustomFields(done));
after(done => clearCustomFields(done));
it('should create a new user', (done) => {
request.post(api('users.create'))
.set(credentials)
.send({
email: apiEmail,
name: apiUsername,
username: apiUsername,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified: true
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', apiUsername);
expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', apiUsername);
it('/users.info:', (done) => {
request.get(api('users.info'))
.set(credentials)
.query({
userId: targetUser._id
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', apiUsername);
expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', apiUsername);
})
.end(done);
expect(res.body).to.not.have.deep.property('user.customFields');
targetUser._id = res.body.user._id;
})
.end(done);
});
it('should create a new user with custom fields', (done) => {
setCustomFields({customFieldText}, (error) => {
if (error) {
return done(error);
}
const username = `customField_${ apiUsername }`;
const email = `customField_${ apiEmail }`;
const customFields = { customFieldText: 'success' };
request.post(api('users.create'))
.set(credentials)
.send({
email,
name: username,
username,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified: true,
customFields
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', username);
expect(res.body).to.have.deep.property('user.emails[0].address', email);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', username);
expect(res.body).to.have.deep.property('user.customFields.customFieldText', 'success');
})
.end(done);
});
});
function failUserWithCustomField(field) {
it(`should not create a user if a custom field ${ field.reason }`, (done) => {
setCustomFields({ customFieldText }, (error) => {
if (error) {
return done(error);
}
const customFields = {};
customFields[field.name] = field.value;
request.post(api('users.create'))
.set(credentials)
.send({
email: `customField_fail_${ apiEmail }`,
name: `customField_fail_${ apiUsername }`,
username: `customField_fail_${ apiUsername }`,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified: true,
customFields
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-user-registration-custom-field');
})
.end(done);
});
});
}
[
{name: 'customFieldText', value: '', reason: 'is required and missing'},
{name: 'customFieldText', value: '0', reason: 'length is less than minLength'},
{name: 'customFieldText', value: '0123456789-0', reason: 'length is more than maxLength'}
].forEach((field) => { failUserWithCustomField(field); });
});
it('/users.getPresence:', (done) => {
request.get(api('users.getPresence'))
.set(credentials)
.query({
userId: targetUser._id
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('presence', 'offline');
})
.end(done);
describe('[/users.info]', () => {
it('should query information about a user by userId', (done) => {
request.get(api('users.info'))
.set(credentials)
.query({
userId: targetUser._id
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', apiUsername);
expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', apiUsername);
})
.end(done);
});
});
it('/users.list:', (done) => {
request.get(api('users.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
})
.end(done);
describe('[/users.getPresence]', () => {
it('should query a user\'s presence by userId', (done) => {
request.get(api('users.getPresence'))
.set(credentials)
.query({
userId: targetUser._id
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('presence', 'offline');
})
.end(done);
});
});
it.skip('/users.list:', (done) => {
//filtering user list
request.get(api('users.list'))
.set(credentials)
.query({
name: { '$regex': 'g' }
})
.field('username', 1)
.sort('createdAt', -1)
.expect(log)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
})
.end(done);
describe('[/users.list]', () => {
it('should query all users in the system', (done) => {
request.get(api('users.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
})
.end(done);
});
it.skip('should query all users in the system by name', (done) => {
//filtering user list
request.get(api('users.list'))
.set(credentials)
.query({
name: { '$regex': 'g' }
})
.field('username', 1)
.sort('createdAt', -1)
.expect(log)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
})
.end(done);
});
});
it.skip('/users.setAvatar:', (done) => {
request.post(api('users.setAvatar'))
.set(credentials)
.attach('avatarUrl', imgURL)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
describe('[/users.setAvatar]', () => {
it.skip('should set the avatar of the auth user', (done) => {
request.post(api('users.setAvatar'))
.set(credentials)
.attach('avatarUrl', imgURL)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
it('/users.update:', (done) => {
request.post(api('users.update'))
.set(credentials)
.send({
userId: targetUser._id,
data :{
email: apiEmail,
name: `edited${ apiUsername }`,
username: `edited${ apiUsername }`,
password,
active: true,
roles: ['user']
}
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', `edited${ apiUsername }`);
expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', `edited${ apiUsername }`);
})
.end(done);
describe('[/users.update]', () => {
it('should update a user\'s info by userId', (done) => {
request.post(api('users.update'))
.set(credentials)
.send({
userId: targetUser._id,
data :{
email: apiEmail,
name: `edited${ apiUsername }`,
username: `edited${ apiUsername }`,
password,
active: true,
roles: ['user']
}
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.deep.property('user.username', `edited${ apiUsername }`);
expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
expect(res.body).to.have.deep.property('user.active', true);
expect(res.body).to.have.deep.property('user.name', `edited${ apiUsername }`);
})
.end(done);
});
});
describe('[/users.createToken]', () => {

@ -316,7 +316,13 @@ describe('[Administration]', () => {
});
it('it should show the role dropdown', () => {
flexTab.usersAddUserRole.isVisible().should.be.true;
flexTab.usersAddUserRoleList.waitForVisible(5000);
flexTab.usersAddUserRoleList.isVisible().should.be.true;
});
it('ít should show the add role button', () => {
flexTab.usersAddUserRoleButton.waitForVisible(5000);
flexTab.usersAddUserRoleButton.isVisible().should.be.true;
});
it('it should show the join default channel checkbox', () => {

@ -97,6 +97,7 @@ describe('[Permissions]', () => {
flexTab.usersAddUserVerifiedCheckbox.click();
flexTab.usersAddUserPassword.setValue(password);
flexTab.usersAddUserChangePasswordCheckbox.click();
flexTab.addRole('user');
flexTab.usersButtonSave.click();
});

@ -85,8 +85,9 @@ class FlexTab extends Page {
get usersAddUserName() { return browser.element('#name'); }
get usersAddUserUsername() { return browser.element('#username'); }
get usersAddUserEmail() { return browser.element('#email'); }
get usersAddUserRoleList() { return browser.element('#roleSelect'); }
get usersAddUserPassword() { return browser.element('#password'); }
get usersAddUserRole() { return browser.element('#role'); }
get usersAddUserRoleButton() { return browser.element('#addRole'); }
get usersAddUserVerifiedCheckbox() { return browser.element('#verified'); }
get usersAddUserChangePasswordCheckbox() { return browser.element('#changePassword'); }
get usersAddUserDefaultChannelCheckbox() { return browser.element('#joinDefaultChannels'); }
@ -122,6 +123,16 @@ class FlexTab extends Page {
this.removeUserBtn.click();
}
addRole(role) {
this.usersAddUserRoleList.waitForVisible(5000);
this.usersAddUserRoleList.click();
browser.waitForVisible(`option[value=${ role }]`, 5000);
browser.click(`option[value=${ role }]`);
this.usersAddUserRoleButton.waitForVisible(5000);
this.usersAddUserRoleButton.click();
browser.waitForVisible(`.remove-role=${ role }`);
}
operateFlexTab(desiredTab, desiredState) {
//desiredState true=open false=closed
switch (desiredTab) {

Loading…
Cancel
Save