diff --git a/.docker/latest/Dockerfile b/.docker/Dockerfile similarity index 96% rename from .docker/latest/Dockerfile rename to .docker/Dockerfile index 77b309ea5830e8d464d598370aef0321da4d31b0..beaccef6157ceb62633a4485062fcab562e44d36 100644 --- a/.docker/latest/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM rocketchat/base:4 -ENV RC_VERSION latest +ENV RC_VERSION 0.57.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.docker/develop/Dockerfile b/.docker/develop/Dockerfile deleted file mode 100644 index b14fd89de4d41f1f32603511ffaefae9e80bda9f..0000000000000000000000000000000000000000 --- a/.docker/develop/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -FROM rocketchat/base:4 - -ENV RC_VERSION develop - -MAINTAINER buildmaster@rocket.chat - -RUN set -x \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/download" -o rocket.chat.tgz \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/asc" -o rocket.chat.tgz.asc \ - && mkdir /app \ - && gpg --verify rocket.chat.tgz.asc \ - && mkdir -p /app \ - && tar -zxf rocket.chat.tgz -C /app \ - && rm rocket.chat.tgz rocket.chat.tgz.asc \ - && cd /app/bundle/programs/server \ - && npm install \ - && npm cache clear \ - && chown -R rocketchat:rocketchat /app - -USER rocketchat - -VOLUME /app/uploads - -WORKDIR /app/bundle - -# needs a mongoinstance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - MONGO_OPLOG_URL=mongodb://mongo:27017/local \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - -EXPOSE 3000 - -CMD ["node", "main.js"] diff --git a/.docker/release-candidate/Dockerfile b/.docker/release-candidate/Dockerfile deleted file mode 100644 index 192b8e95c0194a5bdae2bdc18dc304b9ce967112..0000000000000000000000000000000000000000 --- a/.docker/release-candidate/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM rocketchat/base:4 - -ENV RC_VERSION release-candidate - -MAINTAINER buildmaster@rocket.chat - -RUN set -x \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/download" -o rocket.chat.tgz \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/asc" -o rocket.chat.tgz.asc \ - && mkdir /app \ - && gpg --verify rocket.chat.tgz.asc \ - && mkdir -p /app \ - && tar -zxf rocket.chat.tgz -C /app \ - && rm rocket.chat.tgz rocket.chat.tgz.asc \ - && cd /app/bundle/programs/server \ - && npm install \ - && npm cache clear \ - && chown -R rocketchat:rocketchat /app - -USER rocketchat - -VOLUME /app/uploads - -WORKDIR /app/bundle - -# needs a mongoinstance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - -EXPOSE 3000 - -CMD ["node", "main.js"] diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp index 14ee2003b1780a2fb399406ffd9f6c1fa64bf459..c794307a7de6917a4eb18b8951ad8053743c6d8b 100644 --- a/.sandstorm/sandstorm-pkgdef.capnp +++ b/.sandstorm/sandstorm-pkgdef.capnp @@ -21,7 +21,7 @@ const pkgdef :Spk.PackageDefinition = ( appVersion = 62, # Increment this for every release. - appMarketingVersion = (defaultText = "0.56.0-develop"), + appMarketingVersion = (defaultText = "0.57.0-develop"), # Human-readable representation of appVersion. Should match the way you # identify versions of your app in documentation and marketing. diff --git a/.scripts/set-version.js b/.scripts/set-version.js new file mode 100644 index 0000000000000000000000000000000000000000..c1e1264ae468a560e2039043dd9c5dafa664c7e1 --- /dev/null +++ b/.scripts/set-version.js @@ -0,0 +1,190 @@ +/* eslint object-shorthand: 0, prefer-template: 0 */ + +const path = require('path'); +const fs = require('fs'); +const semver = require('semver'); +const inquirer = require('inquirer'); +const execSync = require('child_process').execSync; +const git = require('simple-git')(process.cwd()); + + +let pkgJson = {}; + +try { + pkgJson = require(path.resolve( + process.cwd(), + './package.json' + )); +} catch (err) { + console.error('no root package.json found'); +} + +const files = [ + './package.json', + './.sandstorm/sandstorm-pkgdef.capnp', + './.travis/snap.sh', + './.docker/Dockerfile', + './packages/rocketchat-lib/rocketchat.info' +]; + +class Actions { + static release_rc() { + function processVersion(version) { + // console.log('Updating files to version ' + version); + + files.forEach(function(file) { + const data = fs.readFileSync(file, 'utf8'); + + fs.writeFileSync(file, data.replace(pkgJson.version, version), 'utf8'); + }); + + execSync('conventional-changelog --config .github/changelog.js -i HISTORY.md -s'); + + inquirer.prompt([{ + type: 'confirm', + message: 'Commit files?', + name: 'commit' + }]).then(function(answers) { + if (!answers.commit) { + return; + } + + git.status((error, status) => { + inquirer.prompt([{ + type: 'checkbox', + message: 'Select files to commit?', + name: 'files', + choices: status.files.map(file => { return {name: `${ file.working_dir } ${ file.path }`, checked: true}; }) + }]).then(function(answers) { + if (answers.files.length) { + git.add(answers.files.map(file => file.slice(2)), () => { + git.commit(`Bump version to ${ version }`, () => { + inquirer.prompt([{ + type: 'confirm', + message: `Add tag ${ version }?`, + name: 'tag' + }]).then(function(answers) { + if (answers.tag) { + // TODO: Add annotated tag + git.addTag(version); + // TODO: Push + // Useg GitHub api to create the release with history + } + }); + }); + }); + } + }); + }); + }); + } + + + inquirer.prompt([{ + type: 'list', + message: `The current version is ${ pkgJson.version }. Update to version:`, + name: 'version', + choices: [ + semver.inc(pkgJson.version, 'prerelease', 'rc'), + // semver.inc(pkgJson.version, 'patch'), + 'custom' + ] + }]).then(function(answers) { + if (answers.version === 'custom') { + inquirer.prompt([{ + name: 'version', + message: 'Enter your custom version:' + }]).then(function(answers) { + processVersion(answers.version); + }); + } else { + processVersion(answers.version); + } + }); + } + + static release_gm() { + function processVersion(version) { + // console.log('Updating files to version ' + version); + + files.forEach(function(file) { + const data = fs.readFileSync(file, 'utf8'); + + fs.writeFileSync(file, data.replace(pkgJson.version, version), 'utf8'); + }); + + execSync('conventional-changelog --config .github/changelog.js -i HISTORY.md -s'); + // TODO improve HISTORY generation for GM + + inquirer.prompt([{ + type: 'confirm', + message: 'Commit files?', + name: 'commit' + }]).then(function(answers) { + if (!answers.commit) { + return; + } + + git.status((error, status) => { + inquirer.prompt([{ + type: 'checkbox', + message: 'Select files to commit?', + name: 'files', + choices: status.files.map(file => { return {name: `${ file.working_dir } ${ file.path }`, checked: true}; }) + }]).then(function(answers) { + if (answers.files.length) { + git.add(answers.files.map(file => file.slice(2)), () => { + git.commit(`Bump version to ${ version }`, () => { + inquirer.prompt([{ + type: 'confirm', + message: `Add tag ${ version }?`, + name: 'tag' + }]).then(function(answers) { + if (answers.tag) { + // TODO: Add annotated tag + git.addTag(version); + // TODO: Push + // Useg GitHub api to create the release with history + } + }); + }); + }); + } + }); + }); + }); + } + + + inquirer.prompt([{ + type: 'list', + message: `The current version is ${ pkgJson.version }. Update to version:`, + name: 'version', + choices: [ + semver.inc(pkgJson.version, 'patch'), + 'custom' + ] + }]).then(function(answers) { + if (answers.version === 'custom') { + inquirer.prompt([{ + name: 'version', + message: 'Enter your custom version:' + }]).then(function(answers) { + processVersion(answers.version); + }); + } else { + processVersion(answers.version); + } + }); + } +} + +git.status((err, status) => { + if (status.current === 'release-candidate') { + Actions.release_rc(); + } else if (status.current === 'master') { + Actions.release_gm(); + } else { + console.log(`No release action for branch ${ status.current }`); + } +}); diff --git a/.scripts/version.js b/.scripts/version.js index a99cf279946879f0e0af948e30b6c342d229c9d5..c4aeb68acd0f2a454578c6903dd2ffef72ae722e 100644 --- a/.scripts/version.js +++ b/.scripts/version.js @@ -1,8 +1,7 @@ /* eslint object-shorthand: 0, prefer-template: 0 */ const path = require('path'); -const fs = require('fs'); -let pkgJson = {}; +var pkgJson = {}; try { pkgJson = require(path.resolve( @@ -13,35 +12,4 @@ try { console.error('no root package.json found'); } -const readline = require('readline'); - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); - -const files = [ - './package.json', - './.sandstorm/sandstorm-pkgdef.capnp', - './.travis/snap.sh', - './packages/rocketchat-lib/rocketchat.info' -]; - -console.log('Current version:', pkgJson.version); -rl.question('New version: ', function(version) { - rl.close(); - - version = version.trim(); - - if (version === '') { - return; - } - - console.log('Updating files to version ' + version); - - files.forEach(function(file) { - const data = fs.readFileSync(file, 'utf8'); - - fs.writeFileSync(file, data.replace(pkgJson.version, version), 'utf8'); - }); -}); +console.log(pkgJson.version); diff --git a/.travis.yml b/.travis.yml index bcb12be39a55db180baed1743cfb2ea4627a194b..948ec8f880c734bcd2ac68fbce88a8e903c15f03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ services: branches: only: - develop - - release-candidate - "/^\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?$/" git: depth: 1 @@ -74,16 +73,24 @@ before_deploy: - ".travis/namefiles.sh" - echo ".travis/sandstorm.sh" deploy: - provider: s3 - access_key_id: AKIAIKIA7H7D47KUHYCA - secret_access_key: "$ACCESSKEY" - bucket: download.rocket.chat - skip_cleanup: true - upload_dir: build - local_dir: "$ROCKET_DEPLOY_DIR" - on: - condition: "$TRAVIS_PULL_REQUEST=false" - all_branches: true + - provider: s3 + access_key_id: AKIAIKIA7H7D47KUHYCA + secret_access_key: "$ACCESSKEY" + bucket: download.rocket.chat + skip_cleanup: true + upload_dir: build + local_dir: "$ROCKET_DEPLOY_DIR" + on: + condition: "$TRAVIS_PULL_REQUEST=false" + all_branches: true + # - provider: releases + # api-key: "$GITHUB_TOKEN" + # file_glob: true + # file: build/* + # skip_cleanup: true + # on: + # tags: true + after_deploy: - ".travis/docker.sh" - ".travis/update-releases.sh" diff --git a/.travis/setartname.sh b/.travis/setartname.sh index cf8f9a150c9a00d168854f2442dfec92ebe43c85..35ba8056881a895d234011429ff63a2eb397fec7 100755 --- a/.travis/setartname.sh +++ b/.travis/setartname.sh @@ -1,6 +1,6 @@ -if [[ $TRAVIS_TAG ]] +if [[ $TRAVIS_BRANCH ]] then - export ARTIFACT_NAME="$TRAVIS_TAG"; + export ARTIFACT_NAME="$(meteor npm run version --silent).$TRAVIS_BUILD_NUMBER" else - export ARTIFACT_NAME="$TRAVIS_BRANCH"; + export ARTIFACT_NAME="$(meteor npm run version --silent)" fi diff --git a/.travis/snap.sh b/.travis/snap.sh index d5ad25ba5c6805828b8d0d7f250f2be024cf7ee0..8b602fa558c03cee5e58c6c374cde8794cb63d24 100755 --- a/.travis/snap.sh +++ b/.travis/snap.sh @@ -17,7 +17,7 @@ elif [[ $TRAVIS_TAG ]]; then RC_VERSION=$TRAVIS_TAG else CHANNEL=edge - RC_VERSION=0.56.0-develop + RC_VERSION=0.57.0-develop fi echo "Preparing to trigger a snap release for $CHANNEL channel" diff --git a/HISTORY.md b/HISTORY.md index a286b0d5590e65f494efeb5d6d0d664d4125b08e..4e732d59c737c668eb5679b4047dc6e80db3df0d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,29 +1,199 @@ -<a name="0.56.0-develop"></a> -# 0.56.0-develop (2017-04-18) - +<a name="0.56.0"></a> +# 0.56.0 (2017-05-15) ### New Features +- [#6881](https://github.com/RocketChat/Rocket.Chat/pull/6881) Add a pointer cursor to message images - [#6615](https://github.com/RocketChat/Rocket.Chat/pull/6615) Add a setting to not run outgoing integrations on message edits +- [#5373](https://github.com/RocketChat/Rocket.Chat/pull/5373) Add option on Channel Settings: Hide Notifications and Hide Unread Room Status ([#2707](https://github.com/RocketChat/Rocket.Chat/issue/2707), [#2143](https://github.com/RocketChat/Rocket.Chat/issue/2143)) +- [#6807](https://github.com/RocketChat/Rocket.Chat/pull/6807) create a method 'create token' +- [#6827](https://github.com/RocketChat/Rocket.Chat/pull/6827) Make channels.info accept roomName, just like groups.info +- [#6797](https://github.com/RocketChat/Rocket.Chat/pull/6797) Option to allow to signup as anonymous +- [#6722](https://github.com/RocketChat/Rocket.Chat/pull/6722) Remove lesshat +- [#6842](https://github.com/RocketChat/Rocket.Chat/pull/6842) Snap ARM support - [#6692](https://github.com/RocketChat/Rocket.Chat/pull/6692) Use tokenSentVia parameter for clientid/secret to token endpoint +- [#6940](https://github.com/RocketChat/Rocket.Chat/pull/6940) Add SMTP settings for Protocol and Pool +- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release +- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page ### Bug Fixes +- [#6845](https://github.com/RocketChat/Rocket.Chat/pull/6845) Added helper for testing if the current user matches the params +- [#6737](https://github.com/RocketChat/Rocket.Chat/pull/6737) Archiving Direct Messages +- [#6734](https://github.com/RocketChat/Rocket.Chat/pull/6734) Bug with incoming integration (0.55.1) +- [#6768](https://github.com/RocketChat/Rocket.Chat/pull/6768) CSV importer: require that there is some data in the zip, not ALL data - [#6709](https://github.com/RocketChat/Rocket.Chat/pull/6709) emoji picker exception +- [#6721](https://github.com/RocketChat/Rocket.Chat/pull/6721) Fix Caddy by forcing go 1.7 as needed by one of caddy's dependencies +- [#6798](https://github.com/RocketChat/Rocket.Chat/pull/6798) Fix iframe wise issues +- [#6704](https://github.com/RocketChat/Rocket.Chat/pull/6704) Fix message types +- [#6760](https://github.com/RocketChat/Rocket.Chat/pull/6760) Hides nav buttons when selecting own profile +- [#6747](https://github.com/RocketChat/Rocket.Chat/pull/6747) Incorrect error message when creating channel +- [#6800](https://github.com/RocketChat/Rocket.Chat/pull/6800) Quoted and replied messages not retaining the original message's alias +- [#6796](https://github.com/RocketChat/Rocket.Chat/pull/6796) REST API user.update throwing error due to rate limiting +- [#6767](https://github.com/RocketChat/Rocket.Chat/pull/6767) Search full name on client side +- [#6758](https://github.com/RocketChat/Rocket.Chat/pull/6758) Sort by real name if use real name setting is enabled +- [#6861](https://github.com/RocketChat/Rocket.Chat/pull/6861) start/unstar message +- [#6896](https://github.com/RocketChat/Rocket.Chat/pull/6896) Users status on main menu always offline +- [#6923](https://github.com/RocketChat/Rocket.Chat/pull/6923) Not showing unread count on electron app’s icon +- [#6939](https://github.com/RocketChat/Rocket.Chat/pull/6939) Compile CSS color variables +- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype +- [#6955](https://github.com/RocketChat/Rocket.Chat/pull/6955) Remove spaces from env PORT and INSTANCE_IP +- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c + + +<details> +<summary>Others</summary> + +- [#5986](https://github.com/RocketChat/Rocket.Chat/pull/5986) Anonymous use +- [#6368](https://github.com/RocketChat/Rocket.Chat/pull/6368) Breaking long URLS to prevent overflow +- [#6671](https://github.com/RocketChat/Rocket.Chat/pull/6671) Convert Katex Package to Js +- [#6780](https://github.com/RocketChat/Rocket.Chat/pull/6780) Convert Mailer Package to Js +- [#6694](https://github.com/RocketChat/Rocket.Chat/pull/6694) Convert markdown to js +- [#6689](https://github.com/RocketChat/Rocket.Chat/pull/6689) Convert Mentions-Flextab Package to Js +- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js +- [#6688](https://github.com/RocketChat/Rocket.Chat/pull/6688) Convert Oembed Package to Js +- [#6672](https://github.com/RocketChat/Rocket.Chat/pull/6672) Converted rocketchat-lib 3 +- [#6654](https://github.com/RocketChat/Rocket.Chat/pull/6654) disable proxy configuration +- [#6816](https://github.com/RocketChat/Rocket.Chat/pull/6816) LingoHub based on develop +- [#6715](https://github.com/RocketChat/Rocket.Chat/pull/6715) LingoHub based on develop +- [#6703](https://github.com/RocketChat/Rocket.Chat/pull/6703) LingoHub based on develop +- [#6858](https://github.com/RocketChat/Rocket.Chat/pull/6858) Meteor update +- [#6706](https://github.com/RocketChat/Rocket.Chat/pull/6706) meteor update to 1.4.4 +- [#6804](https://github.com/RocketChat/Rocket.Chat/pull/6804) Missing useful fields in admin user list [#5110](https://github.com/RocketChat/Rocket.Chat/issue/5110) +- [#6593](https://github.com/RocketChat/Rocket.Chat/pull/6593) Rocketchat lib2 +</details> + + + +<details> +<summary>Details</summary> + +## 0.56.0-rc.7 (2017-05-15) + + +### Bug Fixes + +- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c + + +## 0.56.0-rc.5 (2017-05-11) + + +### Bug Fixes + +- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype +- [#6955](https://github.com/RocketChat/Rocket.Chat/pull/6955) Remove spaces from env PORT and INSTANCE_IP + + +## 0.56.0-rc.4 (2017-05-11) + + +### New Features + +- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page + + +## 0.56.0-rc.3 (2017-05-10) + + +### New Features + +- [#6940](https://github.com/RocketChat/Rocket.Chat/pull/6940) Add SMTP settings for Protocol and Pool +- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release + + +### Bug Fixes + +- [#6939](https://github.com/RocketChat/Rocket.Chat/pull/6939) Compile CSS color variables + + +## 0.56.0-rc.2 (2017-05-09) + + +### Bug Fixes + +- [#6923](https://github.com/RocketChat/Rocket.Chat/pull/6923) Not showing unread count on electron app’s icon + + +## 0.56.0-rc.1 (2017-05-05) + + +### Bug Fixes + +- [#6896](https://github.com/RocketChat/Rocket.Chat/pull/6896) Users status on main menu always offline + + +## 0.56.0-rc.0 (2017-05-04) + + +### New Features + +- [#6881](https://github.com/RocketChat/Rocket.Chat/pull/6881) Add a pointer cursor to message images +- [#6615](https://github.com/RocketChat/Rocket.Chat/pull/6615) Add a setting to not run outgoing integrations on message edits +- [#5373](https://github.com/RocketChat/Rocket.Chat/pull/5373) Add option on Channel Settings: Hide Notifications and Hide Unread Room Status ([#2707](https://github.com/RocketChat/Rocket.Chat/issue/2707), [#2143](https://github.com/RocketChat/Rocket.Chat/issue/2143)) +- [#6807](https://github.com/RocketChat/Rocket.Chat/pull/6807) create a method 'create token' +- [#6827](https://github.com/RocketChat/Rocket.Chat/pull/6827) Make channels.info accept roomName, just like groups.info +- [#6797](https://github.com/RocketChat/Rocket.Chat/pull/6797) Option to allow to signup as anonymous +- [#6722](https://github.com/RocketChat/Rocket.Chat/pull/6722) Remove lesshat +- [#6842](https://github.com/RocketChat/Rocket.Chat/pull/6842) Snap ARM support +- [#6692](https://github.com/RocketChat/Rocket.Chat/pull/6692) Use tokenSentVia parameter for clientid/secret to token endpoint + + +### Bug Fixes + +- [#6845](https://github.com/RocketChat/Rocket.Chat/pull/6845) Added helper for testing if the current user matches the params +- [#6737](https://github.com/RocketChat/Rocket.Chat/pull/6737) Archiving Direct Messages +- [#6734](https://github.com/RocketChat/Rocket.Chat/pull/6734) Bug with incoming integration (0.55.1) +- [#6768](https://github.com/RocketChat/Rocket.Chat/pull/6768) CSV importer: require that there is some data in the zip, not ALL data - [#6709](https://github.com/RocketChat/Rocket.Chat/pull/6709) emoji picker exception +- [#6721](https://github.com/RocketChat/Rocket.Chat/pull/6721) Fix Caddy by forcing go 1.7 as needed by one of caddy's dependencies +- [#6798](https://github.com/RocketChat/Rocket.Chat/pull/6798) Fix iframe wise issues - [#6704](https://github.com/RocketChat/Rocket.Chat/pull/6704) Fix message types +- [#6760](https://github.com/RocketChat/Rocket.Chat/pull/6760) Hides nav buttons when selecting own profile +- [#6747](https://github.com/RocketChat/Rocket.Chat/pull/6747) Incorrect error message when creating channel +- [#6800](https://github.com/RocketChat/Rocket.Chat/pull/6800) Quoted and replied messages not retaining the original message's alias +- [#6796](https://github.com/RocketChat/Rocket.Chat/pull/6796) REST API user.update throwing error due to rate limiting +- [#6767](https://github.com/RocketChat/Rocket.Chat/pull/6767) Search full name on client side +- [#6758](https://github.com/RocketChat/Rocket.Chat/pull/6758) Sort by real name if use real name setting is enabled +- [#6861](https://github.com/RocketChat/Rocket.Chat/pull/6861) start/unstar message <details> <summary>Others</summary> +- [#5986](https://github.com/RocketChat/Rocket.Chat/pull/5986) Anonymous use +- [#6368](https://github.com/RocketChat/Rocket.Chat/pull/6368) Breaking long URLS to prevent overflow +- [#6671](https://github.com/RocketChat/Rocket.Chat/pull/6671) Convert Katex Package to Js +- [#6780](https://github.com/RocketChat/Rocket.Chat/pull/6780) Convert Mailer Package to Js +- [#6694](https://github.com/RocketChat/Rocket.Chat/pull/6694) Convert markdown to js +- [#6689](https://github.com/RocketChat/Rocket.Chat/pull/6689) Convert Mentions-Flextab Package to Js +- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js +- [#6688](https://github.com/RocketChat/Rocket.Chat/pull/6688) Convert Oembed Package to Js +- [#6672](https://github.com/RocketChat/Rocket.Chat/pull/6672) Converted rocketchat-lib 3 +- [#6654](https://github.com/RocketChat/Rocket.Chat/pull/6654) disable proxy configuration +- [#6816](https://github.com/RocketChat/Rocket.Chat/pull/6816) LingoHub based on develop +- [#6715](https://github.com/RocketChat/Rocket.Chat/pull/6715) LingoHub based on develop - [#6703](https://github.com/RocketChat/Rocket.Chat/pull/6703) LingoHub based on develop +- [#6858](https://github.com/RocketChat/Rocket.Chat/pull/6858) Meteor update - [#6706](https://github.com/RocketChat/Rocket.Chat/pull/6706) meteor update to 1.4.4 +- [#6804](https://github.com/RocketChat/Rocket.Chat/pull/6804) Missing useful fields in admin user list [#5110](https://github.com/RocketChat/Rocket.Chat/issue/5110) +- [#6593](https://github.com/RocketChat/Rocket.Chat/pull/6593) Rocketchat lib2 +</details> + </details> +<a name="0.55.1"></a> +## 0.55.1 (2017-04-19) + + +### Bug Fixes + +- [#6734](https://github.com/RocketChat/Rocket.Chat/pull/6734) Bug with incoming integration (0.55.1) + + <a name="0.55.0"></a> # 0.55.0 (2017-04-18) diff --git a/package.json b/package.json index ec9b0a5a746f38616e617fe4fbd008d905bfea9b..57e35a696e7384ef8627163b81897c37945b7f6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "0.56.0-develop", + "version": "0.57.0-develop", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" @@ -56,7 +56,8 @@ "coverage": "nyc -r html mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", "version": "node .scripts/version.js", - "release": "npm run version && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" + "set-version": "node .scripts/set-version.js", + "release": "npm run set-version --silent" }, "license": "MIT", "repository": { @@ -68,11 +69,9 @@ "email": "support@rocket.chat" }, "devDependencies": { - "babel-mocha-es6-compiler": "^0.1.0", - "babel-plugin-array-includes": "^2.0.3", - "chimp": "^0.48.0", - "conventional-changelog": "^1.1.3", - "babel-mocha-es6-compiler": "^0.1.0", + "chimp": "^0.49.0", + "babel-mocha-es6-compiler": "^0.1.0", + "babel-plugin-array-includes": "^2.0.3", "eslint": "^3.19.0", "stylelint": "^7.10.1", "supertest": "^3.0.0" @@ -84,7 +83,7 @@ "file-type": "^4.3.0", "highlight.js": "^9.11.0", "jquery": "^3.2.1", - "mime-db": "^1.27.0", + "mime-db": "^1.28.0", "mime-type": "^3.0.4", "moment": "^2.18.1", "moment-timezone": "^0.5.13", diff --git a/packages/meteor-accounts-saml/saml_utils.js b/packages/meteor-accounts-saml/saml_utils.js index 5d8396e88eca33163aea4136d608eed2b3ec1f3c..02660f2dcdc8657374445f65fcd176cb2efa7118 100644 --- a/packages/meteor-accounts-saml/saml_utils.js +++ b/packages/meteor-accounts-saml/saml_utils.js @@ -432,12 +432,38 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { let decryptionCert; SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { - let keyDescriptor = null; - if (!decryptionCert) { decryptionCert = this.options.privateCert; } + if (!this.options.callbackUrl && !callbackUrl) { + throw new Error( + 'Unable to generate service provider metadata when callbackUrl option is not set'); + } + + const metadata = { + 'EntityDescriptor': { + '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', + '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', + '@entityID': this.options.issuer, + 'SPSSODescriptor': { + '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', + 'SingleLogoutService': { + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, + '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/` + }, + 'NameIDFormat': this.options.identifierFormat, + 'AssertionConsumerService': { + '@index': '1', + '@isDefault': 'true', + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + '@Location': callbackUrl + } + } + } + }; + if (this.options.privateKey) { if (!decryptionCert) { throw new Error( @@ -448,7 +474,7 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, ''); decryptionCert = decryptionCert.replace(/\r\n/g, '\n'); - keyDescriptor = { + metadata['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'] = { 'ds:KeyInfo': { 'ds:X509Data': { 'ds:X509Certificate': { @@ -477,35 +503,6 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { }; } - if (!this.options.callbackUrl && !callbackUrl) { - throw new Error( - 'Unable to generate service provider metadata when callbackUrl option is not set'); - } - - const metadata = { - 'EntityDescriptor': { - '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', - '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', - '@entityID': this.options.issuer, - 'SPSSODescriptor': { - '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', - 'KeyDescriptor': keyDescriptor, - 'SingleLogoutService': { - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, - '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/` - }, - 'NameIDFormat': this.options.identifierFormat, - 'AssertionConsumerService': { - '@index': '1', - '@isDefault': 'true', - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - '@Location': callbackUrl - } - } - } - }; - return xmlbuilder.create(metadata).end({ pretty: true, indent: ' ', diff --git a/packages/meteor-autocomplete/autocomplete-client.coffee b/packages/meteor-autocomplete/autocomplete-client.coffee deleted file mode 100755 index b58875785139042817b8edf03b41f032651c88c2..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/autocomplete-client.coffee +++ /dev/null @@ -1,367 +0,0 @@ -AutoCompleteRecords = new Mongo.Collection("autocompleteRecords") - -isServerSearch = (rule) -> _.isString(rule.collection) - -validateRule = (rule) -> - if rule.subscription? and not Match.test(rule.collection, String) - throw new Error("Collection name must be specified as string for server-side search") - - # XXX back-compat message, to be removed - if rule.callback? - console.warn("autocomplete no longer supports callbacks; use event listeners instead.") - -isWholeField = (rule) -> - # either '' or null both count as whole field. - return !rule.token - -getRegExp = (rule) -> - unless isWholeField(rule) - # Expressions for the range from the last word break to the current cursor position - new RegExp('(^|\\b|\\s)' + rule.token + '([\\w.]*)$') - else - # Whole-field behavior - word characters or spaces - new RegExp('(^)(.*)$') - -getFindParams = (rule, filter, limit) -> - # This is a different 'filter' - the selector from the settings - # We need to extend so that we don't copy over rule.filter - selector = _.extend({}, rule.filter || {}) - options = { limit: limit } - - # Match anything, no sort, limit X - return [ selector, options ] unless filter - - if rule.sort and rule.field - sortspec = {} - # Only sort if there is a filter, for faster performance on a match of anything - sortspec[rule.field] = 1 - options.sort = sortspec - - if _.isFunction(rule.selector) - # Custom selector - _.extend(selector, rule.selector(filter)) - else - selector[rule.field] = { - $regex: if rule.matchAll then filter else "^" + filter - # default is case insensitive search - empty string is not the same as undefined! - $options: if (typeof rule.options is 'undefined') then 'i' else rule.options - } - - return [ selector, options ] - -getField = (obj, str) -> - obj = obj[key] for key in str.split(".") - return obj - -class @AutoComplete - - @KEYS: [ - 40, # DOWN - 38, # UP - 13, # ENTER - 27, # ESCAPE - 9 # TAB - ] - - constructor: (settings) -> - @limit = settings.limit || 5 - @position = settings.position || "bottom" - - @rules = settings.rules - validateRule(rule) for rule in @rules - - @expressions = (getRegExp(rule) for rule in @rules) - - @matched = -1 - @loaded = true - - # Reactive dependencies for current matching rule and filter - @ruleDep = new Deps.Dependency - @filterDep = new Deps.Dependency - @loadingDep = new Deps.Dependency - - # autosubscribe to the record set published by the server based on the filter - # This will tear down server subscriptions when they are no longer being used. - @sub = null - @comp = Deps.autorun => - # Stop any existing sub immediately, don't wait - @sub?.stop() - - return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null - - # subscribe only for server-side collections - unless isServerSearch(rule) - @setLoaded(true) # Immediately loaded - return - - [ selector, options ] = getFindParams(rule, filter, @limit) - - # console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field - @setLoaded(false) - subName = rule.subscription || "autocomplete-recordset" - @sub = Meteor.subscribe(subName, - selector, options, rule.collection, => @setLoaded(true)) - - teardown: -> - # Stop the reactive computation we started for this autocomplete instance - @comp.stop() - - # reactive getters and setters for @filter and the currently matched rule - matchedRule: -> - @ruleDep.depend() - if @matched >= 0 then @rules[@matched] else null - - setMatchedRule: (i) -> - @matched = i - @ruleDep.changed() - - getFilter: -> - @filterDep.depend() - return @filter - - setFilter: (x) -> - @filter = x - @filterDep.changed() - return @filter - - isLoaded: -> - @loadingDep.depend() - return @loaded - - setLoaded: (val) -> - return if val is @loaded # Don't cause redraws unnecessarily - @loaded = val - @loadingDep.changed() - - onKeyUp: -> - return unless @$element # Don't try to do this while loading - startpos = @element.selectionStart - val = @getText().substring(0, startpos) - - ### - Matching on multiple expressions. - We always go from a matched state to an unmatched one - before going to a different matched one. - ### - i = 0 - breakLoop = false - while i < @expressions.length - matches = val.match(@expressions[i]) - - # matching -> not matching - if not matches and @matched is i - @setMatchedRule(-1) - breakLoop = true - - # not matching -> matching - if matches and @matched is -1 - @setMatchedRule(i) - breakLoop = true - - # Did filter change? - if matches and @filter isnt matches[2] - @setFilter(matches[2]) - breakLoop = true - - break if breakLoop - i++ - - onKeyDown: (e) -> - return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0) - - switch e.keyCode - when 9, 13 # TAB, ENTER - if @select() # Don't jump fields or submit if select successful - e.preventDefault() - e.stopPropagation() - # preventDefault needed below to avoid moving cursor when selecting - when 40 # DOWN - e.preventDefault() - @next() - when 38 # UP - e.preventDefault() - @prev() - when 27 # ESCAPE - @$element.blur() - @hideList() - - return - - onFocus: -> - # We need to run onKeyUp after the focus resolves, - # or the caret position (selectionStart) will not be correct - Meteor.defer => @onKeyUp() - - onBlur: -> - # We need to delay this so click events work - # TODO this is a bit of a hack; see if we can't be smarter - Meteor.setTimeout => - @hideList() - , 500 - - onItemClick: (doc, e) => @processSelection(doc, @rules[@matched]) - - onItemHover: (doc, e) -> - @tmplInst.$(".-autocomplete-item").removeClass("selected") - $(e.target).closest(".-autocomplete-item").addClass("selected") - - filteredList: -> - # @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered - filter = @getFilter() # Reactively depend on the filter - return null if @matched is -1 - - rule = @rules[@matched] - # Don't display list unless we have a token or a filter (or both) - # Single field: nothing displayed until something is typed - return null unless rule.token or filter - - [ selector, options ] = getFindParams(rule, filter, @limit) - - Meteor.defer => @ensureSelection() - - # if server collection, the server has already done the filtering work - return AutoCompleteRecords.find({}, options) if isServerSearch(rule) - - # Otherwise, search on client - return rule.collection.find(selector, options) - - isShowing: -> - rule = @matchedRule() - # Same rules as above - showing = rule? and (rule.token or @getFilter()) - - # Do this after the render - if showing - Meteor.defer => - @positionContainer() - @ensureSelection() - - return showing - - # Replace text with currently selected item - select: -> - node = @tmplInst.find(".-autocomplete-item.selected") - return false unless node? - doc = Blaze.getData(node) - return false unless doc # Don't select if nothing matched - - @processSelection(doc, @rules[@matched]) - return true - - processSelection: (doc, rule) -> - replacement = getField(doc, rule.field) - - unless isWholeField(rule) - @replace(replacement, rule) - @hideList() - - else - # Empty string or doesn't exist? - # Single-field replacement: replace whole field - @setText(replacement) - - # Field retains focus, but list is hidden unless another key is pressed - # Must be deferred or onKeyUp will trigger and match again - # TODO this is a hack; see above - @onBlur() - - @$element.trigger("autocompleteselect", doc) - return - - # Replace the appropriate region - replace: (replacement) -> - startpos = @element.selectionStart - fullStuff = @getText() - val = fullStuff.substring(0, startpos) - val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement) - posfix = fullStuff.substring(startpos, fullStuff.length) - separator = (if posfix.match(/^\s/) then "" else " ") - finalFight = val + separator + posfix - @setText finalFight - - newPosition = val.length + 1 - @element.setSelectionRange(newPosition, newPosition) - return - - hideList: -> - @setMatchedRule(-1) - @setFilter(null) - - getText: -> - return @$element.val() || @$element.text() - - setText: (text) -> - if @$element.is("input,textarea") - @$element.val(text) - else - @$element.html(text) - - ### - Rendering functions - ### - positionContainer: -> - # First render; Pick the first item and set css whenever list gets shown - position = @$element.position() - - rule = @matchedRule() - - offset = getCaretCoordinates(@element, @element.selectionStart) - - # In whole-field positioning, we don't move the container and make it the - # full width of the field. - if rule? and isWholeField(rule) - pos = - left: position.left - width: @$element.outerWidth() # position.offsetWidth - else # Normal positioning, at token word - pos = - left: position.left + offset.left - - # Position menu from top (above) or from bottom of caret (below, default) - if @position is "top" - pos.bottom = @$element.offsetParent().height() - position.top - offset.top - else - pos.top = position.top + offset.top + parseInt(@$element.css('font-size')) - - @tmplInst.$(".-autocomplete-container").css(pos) - - ensureSelection : -> - # Re-render; make sure selected item is something in the list or none if list empty - selectedItem = @tmplInst.$(".-autocomplete-item.selected") - - unless selectedItem.length - # Select anything - @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") - - # Select next item in list - next: -> - currentItem = @tmplInst.$(".-autocomplete-item.selected") - return unless currentItem.length # Don't try to iterate an empty list - currentItem.removeClass("selected") - - next = currentItem.next() - if next.length - next.addClass("selected") - else # End of list or lost selection; Go back to first item - @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") - - # Select previous item in list - prev: -> - currentItem = @tmplInst.$(".-autocomplete-item.selected") - return unless currentItem.length # Don't try to iterate an empty list - currentItem.removeClass("selected") - - prev = currentItem.prev() - if prev.length - prev.addClass("selected") - else # Beginning of list or lost selection; Go to end of list - @tmplInst.$(".-autocomplete-item:last-child").addClass("selected") - - # This doesn't need to be reactive because list already changes reactively - # and will cause all of the items to re-render anyway - currentTemplate: -> @rules[@matched].template - -AutocompleteTest = - records: AutoCompleteRecords - getRegExp: getRegExp - getFindParams: getFindParams diff --git a/packages/meteor-autocomplete/autocomplete-server.coffee b/packages/meteor-autocomplete/autocomplete-server.coffee deleted file mode 100755 index 192d5479bb20a72935a8730bb583391fee7100a6..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/autocomplete-server.coffee +++ /dev/null @@ -1,27 +0,0 @@ -class Autocomplete - @publishCursor: (cursor, sub) -> - # This also attaches an onStop callback to sub, so we don't need to worry about that. - # https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js - Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords") - -Meteor.publish 'autocomplete-recordset', (selector, options, collName) -> - collection = global[collName] - unless collection - throw new Error(collName + ' is not defined on the global namespace of the server.') - - # This is a semi-documented Meteor feature: - # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js - unless collection._isInsecure() - Meteor._debug(collName + ' is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.') - return [] # We need this for the subscription to be marked ready - - # guard against client-side DOS: hard limit to 50 - options.limit = Math.min(50, Math.abs(options.limit)) if options.limit - - # Push this into our own collection on the client so they don't interfere with other publications of the named collection. - # This also stops the observer automatically when the subscription is stopped. - Autocomplete.publishCursor( collection.find(selector, options), this) - - # Mark the subscription ready after the initial addition of documents. - this.ready() - diff --git a/packages/meteor-autocomplete/client/autocomplete-client.js b/packages/meteor-autocomplete/client/autocomplete-client.js new file mode 100755 index 0000000000000000000000000000000000000000..809f2fa77043a736705412ca3b105f05abecb7e5 --- /dev/null +++ b/packages/meteor-autocomplete/client/autocomplete-client.js @@ -0,0 +1,449 @@ +/* globals Deps, getCaretCoordinates*/ +const AutoCompleteRecords = new Mongo.Collection('autocompleteRecords'); + +const isServerSearch = function(rule) { + return _.isString(rule.collection); +}; + +const validateRule = function(rule) { + if (rule.subscription != null && !Match.test(rule.collection, String)) { + throw new Error('Collection name must be specified as string for server-side search'); + } + // XXX back-compat message, to be removed + if (rule.callback) { + console.warn('autocomplete no longer supports callbacks; use event listeners instead.'); + } +}; + +const isWholeField = function(rule) { + // either '' or null both count as whole field. + return !rule.token; +}; + +const getRegExp = function(rule) { + if (!isWholeField(rule)) { + // Expressions for the range from the last word break to the current cursor position + return new RegExp(`(^|\\b|\\s)${ rule.token }([\\w.]*)$`); + } else { + // Whole-field behavior - word characters or spaces + return new RegExp('(^)(.*)$'); + } +}; + +const getFindParams = function(rule, filter, limit) { + // This is a different 'filter' - the selector from the settings + // We need to extend so that we don't copy over rule.filter + const selector = _.extend({}, rule.filter || {}); + const options = { + limit + }; + if (!filter) { + // Match anything, no sort, limit X + return [selector, options]; + } + if (rule.sort && rule.field) { + const sortspec = {}; + // Only sort if there is a filter, for faster performance on a match of anything + sortspec[rule.field] = 1; + options.sort = sortspec; + } + if (_.isFunction(rule.selector)) { + // Custom selector + _.extend(selector, rule.selector(filter)); + } else { + selector[rule.field] = { + $regex: rule.matchAll ? filter : `^${ filter }`, + // default is case insensitive search - empty string is not the same as undefined! + $options: typeof rule.options === 'undefined' ? 'i' : rule.options + }; + } + return [selector, options]; +}; + +const getField = function(obj, str) { + const string = str.split('.'); + string.forEach(key => { + obj = obj[key]; + }); + return obj; +}; + +this.AutoComplete = class { + constructor(settings) { + this.KEYS = [40, 38, 13, 27, 9]; + this.limit = settings.limit || 5; + this.position = settings.position || 'bottom'; + this.rules = settings.rules; + const rules = this.rules; + + Object.keys(rules).forEach(key => { + const rule = rules[key]; + validateRule(rule); + }); + + this.expressions = (() => { + return Object.keys(rules).map(key => { + const rule = rules[key]; + return getRegExp(rule); + }); + })(); + this.matched = -1; + this.loaded = true; + + // Reactive dependencies for current matching rule and filter + this.ruleDep = new Deps.Dependency; + this.filterDep = new Deps.Dependency; + this.loadingDep = new Deps.Dependency; + + // Autosubscribe to the record set published by the server based on the filter + // This will tear down server subscriptions when they are no longer being used. + this.sub = null; + this.comp = Deps.autorun(() => { + const rule = this.matchedRule(); + const filter = this.getFilter(); + if (this.sub) { + // Stop any existing sub immediately, don't wait + this.sub.stop(); + } + if (!(rule && filter)) { + return; + } + + // subscribe only for server-side collections + if (!isServerSearch(rule)) { + this.setLoaded(true); + return; + } + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + + // console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field + this.setLoaded(false); + const subName = rule.subscription || 'autocomplete-recordset'; + this.sub = Meteor.subscribe(subName, selector, options, rule.collection, () => { + this.setLoaded(true); + }); + }); + } + + teardown() { + // Stop the reactive computation we started for this autocomplete instance + this.comp.stop(); + } + + matchedRule() { + // reactive getters and setters for @filter and the currently matched rule + this.ruleDep.depend(); + if (this.matched >= 0) { + return this.rules[this.matched]; + } else { + return null; + } + } + + setMatchedRule(i) { + this.matched = i; + this.ruleDep.changed(); + } + + getFilter() { + this.filterDep.depend(); + return this.filter; + } + + setFilter(x) { + this.filter = x; + this.filterDep.changed(); + return this.filter; + } + + isLoaded() { + this.loadingDep.depend(); + return this.loaded; + } + + setLoaded(val) { + if (val === this.loaded) { + return; //Don't cause redraws unnecessarily + } + this.loaded = val; + this.loadingDep.changed(); + } + + onKeyUp() { + if (!this.$element) { + return; //Don't try to do this while loading + } + const startpos = this.element.selectionStart; + const val = this.getText().substring(0, startpos); + + /* + Matching on multiple expressions. + We always go from a matched state to an unmatched one + before going to a different matched one. + */ + let i = 0; + let breakLoop = false; + while (i < this.expressions.length) { + const matches = val.match(this.expressions[i]); + + // matching -> not matching + if (!matches && this.matched === i) { + this.setMatchedRule(-1); + breakLoop = true; + } + + // not matching -> matching + if (matches && this.matched === -1) { + this.setMatchedRule(i); + breakLoop = true; + } + + // Did filter change? + if (matches && this.filter !== matches[2]) { + this.setFilter(matches[2]); + breakLoop = true; + } + if (breakLoop) { + break; + } + i++; + } + } + + onKeyDown(e) { + if (this.matched === -1 || (this.KEYS.indexOf(e.keyCode) < 0)) { + return; + } + switch (e.keyCode) { + case 9: //TAB + case 13: //ENTER + if (this.select()) { //Don't jump fields or submit if select successful + e.preventDefault(); + e.stopPropagation(); + } + break; + // preventDefault needed below to avoid moving cursor when selecting + case 40: //DOWN + e.preventDefault(); + this.next(); + break; + case 38: //UP + e.preventDefault(); + this.prev(); + break; + case 27: //ESCAPE + this.$element.blur(); + this.hideList(); + } + } + + onFocus() { + // We need to run onKeyUp after the focus resolves, + // or the caret position (selectionStart) will not be correct + Meteor.defer(() => this.onKeyUp()); + } + + onBlur() { + // We need to delay this so click events work + // TODO this is a bit of a hack, see if we can't be smarter + Meteor.setTimeout(() => { + this.hideList(); + }, 500); + } + + onItemClick(doc) { + this.processSelection(doc, this.rules[this.matched]); + } + + onItemHover(doc, e) { + this.tmplInst.$('.-autocomplete-item').removeClass('selected'); + $(e.target).closest('.-autocomplete-item').addClass('selected'); + } + + filteredList() { + // @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered + const filter = this.getFilter(); //Reactively depend on the filter + if (this.matched === -1) { + return null; + } + const rule = this.rules[this.matched]; + + // Don't display list unless we have a token or a filter (or both) + // Single field: nothing displayed until something is typed + if (!(rule.token || filter)) { + return null; + } + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + Meteor.defer(() => this.ensureSelection()); + + // if server collection, the server has already done the filtering work + if (isServerSearch(rule)) { + return AutoCompleteRecords.find({}, options); + } + // Otherwise, search on client + return rule.collection.find(selector, options); + } + + isShowing() { + const rule = this.matchedRule(); + // Same rules as above + const showing = rule && (rule.token || this.getFilter()); + + // Do this after the render + if (showing) { + Meteor.defer(() => { + this.positionContainer(); + this.ensureSelection(); + }); + } + return showing; + } + + // Replace text with currently selected item + select() { + const node = this.tmplInst.find('.-autocomplete-item.selected'); + if (node == null) { + return false; + } + const doc = Blaze.getData(node); + if (!doc) { + return false; //Don't select if nothing matched + + } + this.processSelection(doc, this.rules[this.matched]); + return true; + } + + processSelection(doc, rule) { + const replacement = getField(doc, rule.field); + if (!isWholeField(rule)) { + this.replace(replacement, rule); + this.hideList(); + } else { + + // Empty string or doesn't exist? + // Single-field replacement: replace whole field + this.setText(replacement); + + // Field retains focus, but list is hidden unless another key is pressed + // Must be deferred or onKeyUp will trigger and match again + // TODO this is a hack; see above + this.onBlur(); + } + this.$element.trigger('autocompleteselect', doc); + } + + + // Replace the appropriate region + replace(replacement) { + const startpos = this.element.selectionStart; + const fullStuff = this.getText(); + let val = fullStuff.substring(0, startpos); + val = val.replace(this.expressions[this.matched], `$1${ this.rules[this.matched].token }${ replacement }`); + const posfix = fullStuff.substring(startpos, fullStuff.length); + const separator = (posfix.match(/^\s/) ? '' : ' '); + const finalFight = val + separator + posfix; + this.setText(finalFight); + const newPosition = val.length + 1; + this.element.setSelectionRange(newPosition, newPosition); + } + + hideList() { + this.setMatchedRule(-1); + this.setFilter(null); + } + + getText() { + return this.$element.val() || this.$element.text(); + } + + setText(text) { + if (this.$element.is('input,textarea')) { + this.$element.val(text); + } else { + this.$element.html(text); + } + } + + + /* + Rendering functions + */ + + positionContainer() { + // First render; Pick the first item and set css whenever list gets shown + let pos; + const position = this.$element.position(); + const rule = this.matchedRule(); + const offset = getCaretCoordinates(this.element, this.element.selectionStart); + + // In whole-field positioning, we don't move the container and make it the + // full width of the field. + if (rule && isWholeField(rule)) { + pos = { + left: position.left, + width: this.$element.outerWidth() //position.offsetWidth + }; + } else { //Normal positioning, at token word + pos = { left: position.left + offset.left }; + } + + // Position menu from top (above) or from bottom of caret (below, default) + if (this.position === 'top') { + pos.bottom = this.$element.offsetParent().height() - position.top - offset.top; + } else { + pos.top = position.top + offset.top + parseInt(this.$element.css('font-size')); + } + this.tmplInst.$('.-autocomplete-container').css(pos); + } + + ensureSelection() { + // Re-render; make sure selected item is something in the list or none if list empty + const selectedItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!selectedItem.length) { + // Select anything + this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + // Select next item in list + next() { + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; + } + currentItem.removeClass('selected'); + const next = currentItem.next(); + if (next.length) { + next.addClass('selected'); + } else { //End of list or lost selection; Go back to first item + this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + //Select previous item in list + prev() { + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; //Don't try to iterate an empty list + } + currentItem.removeClass('selected'); + const prev = currentItem.prev(); + if (prev.length) { + prev.addClass('selected'); + } else { //Beginning of list or lost selection; Go to end of list + this.tmplInst.$('.-autocomplete-item:last-child').addClass('selected'); + } + } + + // This doesn't need to be reactive because list already changes reactively + // and will cause all of the items to re-render anyway + currentTemplate() { + return this.rules[this.matched].template; + } + +}; diff --git a/packages/meteor-autocomplete/autocomplete.css b/packages/meteor-autocomplete/client/autocomplete.css similarity index 100% rename from packages/meteor-autocomplete/autocomplete.css rename to packages/meteor-autocomplete/client/autocomplete.css diff --git a/packages/meteor-autocomplete/inputs.html b/packages/meteor-autocomplete/client/inputs.html similarity index 100% rename from packages/meteor-autocomplete/inputs.html rename to packages/meteor-autocomplete/client/inputs.html diff --git a/packages/meteor-autocomplete/client/templates.js b/packages/meteor-autocomplete/client/templates.js new file mode 100755 index 0000000000000000000000000000000000000000..97b2e25971355c73ee7880e34c709a9d3746eeab --- /dev/null +++ b/packages/meteor-autocomplete/client/templates.js @@ -0,0 +1,80 @@ +/* globals AutoComplete */ +// Events on template instances, sent to the autocomplete class +const acEvents = { + 'keydown'(e, t) { + t.ac.onKeyDown(e); + }, + 'keyup'(e, t) { + t.ac.onKeyUp(e); + }, + 'focus'(e, t) { + t.ac.onFocus(e); + }, + 'blur'(e, t) { + t.ac.onBlur(e); + } +}; + +Template.inputAutocomplete.events(acEvents); + +Template.textareaAutocomplete.events(acEvents); + +const attributes = function() { + return _.omit(this, 'settings'); //Render all but the settings parameter + +}; + +const autocompleteHelpers = { + attributes, + autocompleteContainer: new Template('AutocompleteContainer', function() { + const ac = new AutoComplete(Blaze.getData().settings); + // Set the autocomplete object on the parent template instance + this.parentView.templateInstance().ac = ac; + + // Set nodes on render in the autocomplete class + this.onViewReady(function() { + ac.element = this.parentView.firstNode(); + ac.$element = $(ac.element); + }); + return Blaze.With(ac, function() { //eslint-disable-line + return Template._autocompleteContainer; + }); + }) +}; + +Template.inputAutocomplete.helpers(autocompleteHelpers); + +Template.textareaAutocomplete.helpers(autocompleteHelpers); + +Template._autocompleteContainer.rendered = function() { + this.data.tmplInst = this; +}; + +Template._autocompleteContainer.destroyed = function() { + // Meteor._debug "autocomplete destroyed" + this.data.teardown(); +}; + + +/* + List rendering helpers + */ + +Template._autocompleteContainer.events({ + // t.data is the AutoComplete instance; `this` is the data item + 'click .-autocomplete-item'(e, t) { + t.data.onItemClick(this, e); + }, + 'mouseenter .-autocomplete-item'(e, t) { + t.data.onItemHover(this, e); + } +}); + +Template._autocompleteContainer.helpers({ + empty() { + return this.filteredList().count() === 0; + }, + noMatchTemplate() { + return this.matchedRule().noMatchTemplate || Template._noMatch; + } +}); diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js index 5a6912af07a94fb162a3a78c14c36212a2c23423..ab887e0432afc1dffa6ef851c651759c87b11481 100755 --- a/packages/meteor-autocomplete/package.js +++ b/packages/meteor-autocomplete/package.js @@ -7,21 +7,21 @@ Package.describe({ Package.onUse(function(api) { api.use(['blaze', 'templating', 'jquery'], 'client'); - api.use(['coffeescript', 'underscore', 'ecmascript']); // both + api.use(['underscore', 'ecmascript']); // both api.use(['mongo', 'ddp']); api.use('dandv:caret-position@2.1.0-3', 'client'); // Our files api.addFiles([ - 'autocomplete.css', - 'inputs.html', - 'autocomplete-client.coffee', - 'templates.coffee' + 'client/autocomplete.css', + 'client/inputs.html', + 'client/autocomplete-client.js', + 'client/templates.js' ], 'client'); api.addFiles([ - 'autocomplete-server.coffee' + 'server/autocomplete-server.js' ], 'server'); api.export('Autocomplete', 'server'); @@ -31,7 +31,6 @@ Package.onUse(function(api) { Package.onTest(function(api) { api.use('mizzao:autocomplete'); - api.use('coffeescript'); api.use('mongo'); api.use('tinytest'); diff --git a/packages/meteor-autocomplete/server/autocomplete-server.js b/packages/meteor-autocomplete/server/autocomplete-server.js new file mode 100755 index 0000000000000000000000000000000000000000..582daf40c1d1a1e6841f58e0a319a199a9b07a7a --- /dev/null +++ b/packages/meteor-autocomplete/server/autocomplete-server.js @@ -0,0 +1,31 @@ +// This also attaches an onStop callback to sub, so we don't need to worry about that. +// https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js +const Autocomplete = class { + publishCursor(cursor, sub) { + Mongo.Collection._publishCursor(cursor, sub, 'autocompleteRecords'); + } +}; + +Meteor.publish('autocomplete-recordset', function(selector, options, collName) { + const collection = global[collName]; + + // This is a semi-documented Meteor feature: + // https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js + if (!collection) { + throw new Error(`${ collName } is not defined on the global namespace of the server.`); + } + if (!collection._isInsecure()) { + Meteor._debug(`${ collName } is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.`); + return []; // We need this for the subscription to be marked ready + } + if (options.limit) { + // guard against client-side DOS: hard limit to 50 + options.limit = Math.min(50, Math.abs(options.limit)); + } + + // Push this into our own collection on the client so they don't interfere with other publications of the named collection. + // This also stops the observer automatically when the subscription is stopped. + Autocomplete.publishCursor(collection.find(selector, options), this); + // Mark the subscription ready after the initial addition of documents. + this.ready(); +}); diff --git a/packages/meteor-autocomplete/templates.coffee b/packages/meteor-autocomplete/templates.coffee deleted file mode 100755 index cd56d8ba739846aa43fde4f8deca69d1eaf5dd63..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/templates.coffee +++ /dev/null @@ -1,50 +0,0 @@ -# Events on template instances, sent to the autocomplete class -acEvents = - "keydown": (e, t) -> t.ac.onKeyDown(e) - "keyup": (e, t) -> t.ac.onKeyUp(e) - "focus": (e, t) -> t.ac.onFocus(e) - "blur": (e, t) -> t.ac.onBlur(e) - -Template.inputAutocomplete.events(acEvents) -Template.textareaAutocomplete.events(acEvents) - -attributes = -> _.omit(@, 'settings') # Render all but the settings parameter - -autocompleteHelpers = { - attributes, - autocompleteContainer: new Template('AutocompleteContainer', -> - ac = new AutoComplete( Blaze.getData().settings ) - # Set the autocomplete object on the parent template instance - this.parentView.templateInstance().ac = ac - - # Set nodes on render in the autocomplete class - this.onViewReady -> - ac.element = this.parentView.firstNode() - ac.$element = $(ac.element) - - return Blaze.With(ac, -> Template._autocompleteContainer) - ) -} - -Template.inputAutocomplete.helpers(autocompleteHelpers) -Template.textareaAutocomplete.helpers(autocompleteHelpers) - -Template._autocompleteContainer.rendered = -> - @data.tmplInst = this - -Template._autocompleteContainer.destroyed = -> - # Meteor._debug "autocomplete destroyed" - @data.teardown() - -### - List rendering helpers -### - -Template._autocompleteContainer.events - # t.data is the AutoComplete instance; `this` is the data item - "click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e) - "mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e) - -Template._autocompleteContainer.helpers - empty: -> @filteredList().count() is 0 - noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch diff --git a/packages/rocketchat-api/server/v1/channels.js b/packages/rocketchat-api/server/v1/channels.js index 1de2d82d724b238b812fdbb812f855044ddf9530..da10b21283d5f280aeb220152e2dcc8d6c36a3e5 100644 --- a/packages/rocketchat-api/server/v1/channels.js +++ b/packages/rocketchat-api/server/v1/channels.js @@ -128,7 +128,7 @@ RocketChat.API.v1.addRoute('channels.close', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.create', { authRequired: true }, { post() { - if (!RocketChat.authz.hasPermission(this.userId, 'create-p')) { + if (!RocketChat.authz.hasPermission(this.userId, 'create-c')) { return RocketChat.API.v1.unauthorized(); } diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index daac0ea19932a009785e5e84f570348d34f8e558..e217c963e3f5f111c13cab42dc7f32609dfd3ecf 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -1,5 +1,5 @@ /* globals Slingshot, FileUpload, AWS, SystemLogger */ -const crypto = Npm.require('crypto'); +import AWS4 from '../lib/AWS4.js'; let S3accessKey; let S3secretKey; @@ -9,11 +9,23 @@ const generateURL = function(file) { if (!file || !file.s3) { return; } - const resourceURL = `/${ file.s3.bucket }/${ file.s3.path }${ file._id }`; - const expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan); - const StringToSign = `GET\n\n\n${ expires }\n${ resourceURL }`; - const signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64'); - return `${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; + + const credential = { + accessKeyId: S3accessKey, + secretKey: S3secretKey + }; + + const req = { + bucket: file.s3.bucket, + region: file.s3.region, + path: `/${ file.s3.path }${ file._id }`, + url: file.url, + expire: Math.max(5, S3expiryTimeSpan) + }; + + const queryString = AWS4.sign(req, credential); + + return `${ file.url }?${ queryString }`; }; FileUpload.addHandler('s3', { diff --git a/packages/rocketchat-file-upload/server/lib/AWS4.js b/packages/rocketchat-file-upload/server/lib/AWS4.js new file mode 100644 index 0000000000000000000000000000000000000000..bb8980ac0d4c79e8cdcb74e1007961e3f0810a1f --- /dev/null +++ b/packages/rocketchat-file-upload/server/lib/AWS4.js @@ -0,0 +1,175 @@ +import crypto from 'crypto'; +import urllib from 'url'; +import querystring from 'querystring'; + +const Algorithm = 'AWS4-HMAC-SHA256'; +const DefaultRegion = 'us-east-1'; +const Service = 's3'; +const KeyPartsRequest = 'aws4_request'; + +class Aws4 { + constructor(req, credentials) { + const { url, method = 'GET', body = '', date, region, headers = {}, expire = 86400 } = this.req = req; + + Object.assign(this, { url, body, method: method.toUpperCase() }); + + const urlObj = urllib.parse(url); + this.region = region || DefaultRegion; + this.path = urlObj.pathname; + this.host = urlObj.host; + this.date = date || this.amzDate; + this.credentials = credentials; + this.headers = this.prepareHeaders(headers); + this.expire = expire; + } + + prepareHeaders() { + const host = this.host; + + return { + host + }; + } + + hmac(key, string, encoding) { + return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding); + } + + hash(string, encoding = 'hex') { + return crypto.createHash('sha256').update(string, 'utf8').digest(encoding); + } + + encodeRfc3986(urlEncodedString) { + return urlEncodedString.replace(/[!'()*]/g, function(c) { + return `%${ c.charCodeAt(0).toString(16).toUpperCase() }`; + }); + } + + encodeQuery(query) { + return this.encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce((obj, key) => { + if (!key) { return obj; } + obj[key] = !Array.isArray(query[key]) ? query[key] : query[key].slice().sort(); + return obj; + }, {}))); + } + + get query() { + const query = {}; + + if (this.credentials.sessionToken) { + query['X-Amz-Security-Token'] = this.credentials.sessionToken; + } + + query['X-Amz-Expires'] = this.expire; + query['X-Amz-Date'] = this.amzDate; + query['X-Amz-Algorithm'] = Algorithm; + query['X-Amz-Credential'] = `${ this.credentials.accessKeyId }/${ this.credentialScope }`; + query['X-Amz-SignedHeaders'] = this.signedHeaders; + + return query; + } + + get amzDate() { + return (new Date()).toISOString().replace(/[:\-]|\.\d{3}/g, ''); + } + + get dateStamp() { + return this.date.slice(0, 8); + } + + get payloadHash() { + return 'UNSIGNED-PAYLOAD'; + } + + get canonicalPath() { + let pathStr = this.path; + if (pathStr === '/') { return pathStr; } + + pathStr = pathStr.replace(/\/{2,}/g, '/'); + pathStr = pathStr.split('/').reduce((path, piece) => { + if (piece === '..') { + path.pop(); + } else { + path.push(this.encodeRfc3986(querystring.escape(piece))); + } + return path; + }, []).join('/'); + + return pathStr; + } + + get canonicalQuery() { + return this.encodeQuery(this.query); + } + + get canonicalHeaders() { + const headers = Object.keys(this.headers) + .sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) + .map(key => `${ key.toLowerCase() }:${ this.headers[key] }`); + return `${ headers.join('\n') }\n`; + } + + get signedHeaders() { + return Object.keys(this.headers) + .map(key => key.toLowerCase()) + .sort() + .join(';'); + } + + get canonicalRequest() { + return [ + this.method, + this.canonicalPath, + this.canonicalQuery, + this.canonicalHeaders, + this.signedHeaders, + this.payloadHash + ].join('\n'); + } + + get credentialScope() { + return [ + this.dateStamp, + this.region, + Service, + KeyPartsRequest + ].join('/'); + } + + get stringToSign() { + return [ + Algorithm, + this.date, + this.credentialScope, + this.hash(this.canonicalRequest) + ].join('\n'); + } + + get signingKey() { + const kDate = this.hmac(`AWS4${ this.credentials.secretKey }`, this.dateStamp); + const kRegion = this.hmac(kDate, this.region); + const kService = this.hmac(kRegion, Service); + const kSigning = this.hmac(kService, KeyPartsRequest); + + return kSigning; + } + + get signature() { + return this.hmac(this.signingKey, this.stringToSign, 'hex'); + } + + // Export + // Return signed query string + sign() { + const query = this.query; + query['X-Amz-Signature'] = this.signature; + + return this.encodeQuery(query); + } +} + +export default { + sign(request, credential) { + return (new Aws4(request, credential)).sign(); + } +}; diff --git a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js index ef48ddc2163b815644a57dd9712f39e474327a69..c3cbca844eb80c638ad2f8cfcef6597f73065537 100644 --- a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js +++ b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js @@ -1,4 +1,5 @@ -const crypto = Npm.require('crypto'); +import AWS4 from '../lib/AWS4.js'; + let protectedFiles; let S3accessKey; let S3secretKey; @@ -26,12 +27,22 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage' }); } const file = RocketChat.models.Uploads.findOneById(fileId); - const resourceURL = `/${ file.s3.bucket }/${ file.s3.path }${ file._id }`; - const expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan); - const StringToSign = `GET\n\n\n${ expires }\n${ resourceURL }`; - const signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64'); - return { - url:`${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }` + + const credential = { + accessKeyId: S3accessKey, + secretKey: S3secretKey + }; + + const req = { + bucket: file.s3.bucket, + region: file.s3.region, + path: `/${ file.s3.path }${ file._id }`, + url: file.url, + expire: Math.max(5, S3expiryTimeSpan) }; + + const queryString = AWS4.sign(req, credential); + + return `${ file.url }?${ queryString }`; } }); diff --git a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json index 0b44b80161cc02c6cbf5334e10d59e939220f616..e171621ad42f49f5aeb214b231cd0a9f40a36a58 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -1138,6 +1138,11 @@ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "from": "is-property@>=1.0.0 <2.0.0" }, + "is-stream-ended": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.3.tgz", + "from": "is-stream-ended@>=0.1.0 <0.2.0" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1408,21 +1413,9 @@ "from": "sntp@>=1.0.0 <2.0.0" }, "split-array-stream": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.2.tgz", - "from": "split-array-stream@>=1.0.0 <2.0.0", - "dependencies": { - "end-of-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", - "from": "end-of-stream@>=1.4.0 <2.0.0" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.4.0 <2.0.0" - } - } + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.3.tgz", + "from": "split-array-stream@>=1.0.0 <2.0.0" }, "sshpk": { "version": "1.13.0", diff --git a/packages/rocketchat-i18n/i18n/ca.i18n.json b/packages/rocketchat-i18n/i18n/ca.i18n.json index 436bcca1e9de01d37326825effe7f30fbfa4fe04..185c793f6340d4d2a15b4a30ace06b25ef5a0bda 100644 --- a/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "L'accés als permisos", "Account_SID": "Compte SID", "Accounts": "Comptes", - "Accounts_AllowAnonymousAccess": "Permet accés anònim", + "Accounts_AllowAnonymousRead": "Permetre lectura anònima", + "Accounts_AllowAnonymousWrite": "Permetre escriptura anònima", "Accounts_AllowDeleteOwnAccount": "Permetre als usuaris eliminar el seu propi compte", "Accounts_AllowedDomainsList": "Llista de dominis permesos", "Accounts_AllowedDomainsList_Description": "Llista dels dominis permesos separada per comes ", @@ -35,6 +36,7 @@ "Accounts_BlockedUsernameList": "Llista de noms d'usuari bloquejats", "Accounts_BlockedUsernameList_Description": "Llista separada per comes de noms d'usuari bloquejats (no distingeix majúscules/minúscules)", "Accounts_CustomFields_Description": "Ha de ser un objecte JSON và lid on les claus són els noms dels camps i contenen un diccionari amb les opcions del camp. Exemple:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", + "Accounts_DefaultUsernamePrefixSuggestion": "Prefix suggerit per al nom d'usuari per defecte", "Accounts_denyUnverifiedEmail": "Denegar correu electrònic sense verificar", "Accounts_EmailVerification": "Verificació de correu electrònic", "Accounts_EmailVerification_Description": "Assegura't que la configuració SMTP és correcta per fer servir aquesta funcionalitat", @@ -265,6 +267,7 @@ "BotHelpers_userFields": "Camps d'usuari", "BotHelpers_userFields_Description": "CSV de camps d'usuari que poden ser accedits pels mètodes helper dels bots.", "Branch": "Branca", + "Broadcast_Connected_Instances": "Difusió de les instà ncies connectades", "Bugsnag_api_key": "Clau API Bugsnag", "busy": "ocupat", "Busy": "Ocupat", @@ -360,6 +363,7 @@ "CROWD_Reject_Unauthorized": "Rebutja no autoritzat", "CRM_Integration": "Integració CRM", "Current_Chats": "Xats actuals", + "Current_Status": "Estat actual", "Custom": "Personalitzat", "Custom_Emoji": "Emoticona personalitzada", "Custom_Emoji_Add": "Afegir nova emoticona", @@ -668,11 +672,11 @@ "Iframe_Integration_receive_enable": "Activa recepció", "Iframe_Integration_receive_enable_Description": "Permetre que la finestra pare enviï ordres a Rocket.Chat.", "Iframe_Integration_receive_origin": "Rebre orÃgens", - "Iframe_Integration_receive_origin_Description": "Només les pà gines de les quals es proporciona l'origen podran enviar ordres o `*` per a totes. Es poden utilitzar múltiples valors separats per `,`. Exemple `http://localhost,https://localhost`", + "Iframe_Integration_receive_origin_Description": "Origens amb prefix del protocol, separats per comes, dels quals es permet rebre comandes. Exemple 'http://localhost, https://localhost', o * per permetre rebre de qualsevol lloc.", "Iframe_Integration_send_enable": "Activa enviament", "Iframe_Integration_send_enable_Description": "Envia esdeveniments a la finestra pare", "Iframe_Integration_send_target_origin": "Envia l'origen a l'objectiu", - "Iframe_Integration_send_target_origin_Description": "Només les pà gines amb l'origen proporcionat podran rebre esdeveniments o `*` per a totes. Exemple `http://localhost`", + "Iframe_Integration_send_target_origin_Description": "Origens amb prefix del protocol on les comandes són enviades. Exemple 'https://localhost', o * per permetre enviar a qualsevol lloc.", "Importer_Archived": "Arxivat", "Importer_CSV_Information": "L'importador CSV requereix un format especÃfic, si us plau llegiu la documentació sobre com estructurar l'arxiu .zip:", "Importer_HipChatEnterprise_Information": "L'arxiu pujat ha de ser un tar.gz desencriptat. Si us plau, llegiu la documentació per a més informació:", @@ -704,6 +708,7 @@ "Install_FxOs_follow_instructions": "Si us plau, confirma la instal·lació de l'aplicació al teu dispositiu (polsi \"Instal·lar\" quan se us demani).", "Installation": "Instal·lació", "Installed_at": "Instal·lat a", + "Instance_Record": "Registre d'instà ncia", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instruccions als visitants, ompliu el formulari per enviar un missatge", "Impersonate_user": "Suplantar usuari", "Impersonate_user_description": "Quan s'activa, la integració publica com a l'usuari que ha desencadenat la integració", @@ -1124,6 +1129,7 @@ "or": "o", "Open_your_authentication_app_and_enter_the_code": "Obre l'app d'autenticació i entra el codi. També pots utilitzar un dels codis de recuperació.", "Order": "Ordre", + "Or_talk_as_anonymous": "O parla com a anònim", "OS_Arch": "Arquitectura del sistema", "OS_Cpus": "Recompte de CPU", "OS_Freemem": "Memòria RAM lliure", @@ -1228,7 +1234,6 @@ "Register": "Crea un compte nou", "Registration": "Registre", "Registration_Succeeded": "Registre reeixit", - "Register_or_login_to_send_messages": "Registra't o identifica't per enviar missatges", "Registration_via_Admin": "Registre via Admin", "Regular_Expressions": "Expressions regulars", "Release": "Llançament", @@ -1254,6 +1259,7 @@ "Reset_password": "Reinicialitza la contrasenya", "Restart": "Reinicia (restart)", "Restart_the_server": "Reinicia el servidor", + "Retry_Count": "Comptador de reintents", "Role": "Rol", "Role_Editing": "Edició de rols", "Role_removed": "Rol eliminat", @@ -1363,6 +1369,7 @@ "Showing_archived_results": "<p>Mostrant <b>%s</b> resultats arxivats</p>", "Showing_online_users": "Mostrant-ne <b>__total_showing__</b>, En lÃnia: __online__, Total: __total__ usuaris", "Showing_results": "<p>Mostrant <b>%s</b> resultats</p>", + "Sign_in_to_start_talking": "Identifica't per començar a parlar", "since_creation": "des de %s", "Site_Name": "Nom del lloc", "Site_Url": "URL del lloc", @@ -1557,6 +1564,7 @@ "Unread_Rooms": "Sales no llegides", "Unread_Rooms_Mode": "Mode de sales no llegides", "Unstar_Message": "Esborra el destacat", + "Updated_at": "Actualitzat el", "Upload_file_description": "Descripció de l'arxiu", "Upload_file_name": "Nom de l'arxiu", "Upload_file_question": "Pujar l'arxiu?", diff --git a/packages/rocketchat-i18n/i18n/cs.i18n.json b/packages/rocketchat-i18n/i18n/cs.i18n.json index ba2dab094d9aaf1329a074484b881bde67931f71..f3d89cf4c104f0353fcbbc415f1047e455fd3b2f 100644 --- a/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "PÅ™Ãstup k oprávnÄ›nÃ", "Account_SID": "SID úÄtu", "Accounts": "ÚÄty", - "Accounts_AllowAnonymousAccess": "Povolit anonymnà pÅ™Ãstup", + "Accounts_AllowAnonymousRead": "Povolit anonymům ÄÃst", + "Accounts_AllowAnonymousWrite": "Povolit anonymům zapisovat", "Accounts_AllowDeleteOwnAccount": "Povolit uživatelům odstranit vlastnà úÄet", "Accounts_AllowedDomainsList": "Seznam povolených domén", "Accounts_AllowedDomainsList_Description": "Čárkami oddÄ›lený seznam povolených domén", @@ -35,6 +36,7 @@ "Accounts_BlockedUsernameList": "Zakázaná uživatelská jména", "Accounts_BlockedUsernameList_Description": "Äárkou oddÄ›lený seznam uživatelských jmen (na velikosti pÃsmen nezáležÃ)", "Accounts_CustomFields_Description": "Validnà JSON obsahujÃcà klÃÄe polà s nastavenÃm. NapÅ™Ãklad:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code>", + "Accounts_DefaultUsernamePrefixSuggestion": "Výchozà návrh prefixu uživatelského jména", "Accounts_denyUnverifiedEmail": "Zakázat neověřené e-mailové adresy", "Accounts_EmailVerification": "Ověřenà e-mailu", "Accounts_EmailVerification_Description": "Pro použità této funkce se ujistÄ›te, že máte správné nastavenà SMTP", @@ -265,6 +267,7 @@ "BotHelpers_userFields": "Uživatelská pole", "BotHelpers_userFields_Description": "CSV uživatelských polÃ, která budou pÅ™Ãstupná botům", "Branch": "VÄ›tev", + "Broadcast_Connected_Instances": "PÅ™ipojené instance", "Bugsnag_api_key": "Bugsnag API klÃÄ", "busy": "zaneprázdnÄ›ný", "Busy": "ZaneprázdnÄ›ný", @@ -360,6 +363,7 @@ "CROWD_Reject_Unauthorized": "ZamÃtnout neutorizované", "CRM_Integration": "Integrace CRM", "Current_Chats": "Aktuálnà MÃstnosti", + "Current_Status": "Aktuálnà stav", "Custom": "VlastnÃ", "Custom_Emoji": "Vlastnà emotikona", "Custom_Emoji_Add": "PÅ™idat novou emotikonu", @@ -704,6 +708,7 @@ "Install_FxOs_follow_instructions": "ProsÃm potvrÄte instalaci aplikace na VaÅ¡em pÅ™Ãstroji (stisknÄ›te tlaÄÃtko \"Install\" po výzvÄ›).", "Installation": "Instalace", "Installed_at": "instalováno v", + "Instance_Record": "ID Instance", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Pokyny pro VaÅ¡e návÅ¡tÄ›vnÃky k vyplnÄ›nà formulář pro odeslánà zprávy", "Impersonate_user": "Vydávat se za uživatele", "Impersonate_user_description": "Pokud je povoleno, integrace posÃlá za uživatele, který ji vyvolal", @@ -1124,6 +1129,7 @@ "or": "nebo", "Open_your_authentication_app_and_enter_the_code": "OtevÅ™ete autentizaÄnà aplikaci a zadejte vygenerovaný kód. Můžete použÃt jeden ze svých záložnÃch kódů.", "Order": "Objednat", + "Or_talk_as_anonymous": "Vydávat sezaanonyma", "OS_Arch": "Architektura OS", "OS_Cpus": "PoÄet CPU OS", "OS_Freemem": "Volná paměť OS", @@ -1228,7 +1234,6 @@ "Register": "Zaregistrovat nový úÄet", "Registration": "Registrace", "Registration_Succeeded": "Registrace úspěšná", - "Register_or_login_to_send_messages": "Pro odeslánà zpráv je tÅ™eba se zaregistrovat nebo pÅ™ihlásit", "Registration_via_Admin": "Registrace pÅ™es Admin", "Regular_Expressions": "Regulárnà výrazy", "Release": "Verze", @@ -1254,6 +1259,7 @@ "Reset_password": "Obnovit heslo", "Restart": "Restartovat", "Restart_the_server": "Restartovat server", + "Retry_Count": "PoÄet opakovánÃ", "Role": "Role", "Role_Editing": "Editace Role", "Role_removed": "Role odstranÄ›na", @@ -1363,6 +1369,7 @@ "Showing_archived_results": "<p> <b>Zobrazeno %s</b> archivovaných výsledků </p>", "Showing_online_users": "Viditelných <b>__total_showing__</b> z __total__ uživatelů", "Showing_results": "<p>Zobrazeno <b>%s</b> výsledků</p>", + "Sign_in_to_start_talking": "Pro konverzaci se pÅ™ihlaÅ¡te", "since_creation": "od %s", "Site_Name": "Jméno stránky", "Site_Url": "URL stránky", @@ -1557,6 +1564,7 @@ "Unread_Rooms": "NepÅ™eÄtené mÃstnosti", "Unread_Rooms_Mode": "Mód NepÅ™eÄtených mÃstnostÃ", "Unstar_Message": "Odebrat hvÄ›zdiÄku", + "Updated_at": "Poslednà aktualizace", "Upload_file_description": "Popis souboru", "Upload_file_name": "Název souboru", "Upload_file_question": "Nahrát soubor?", diff --git a/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 95bf6f0116731ae1a8db1a1a98390008a1ecc203..f89e6d847474833a6b4cd6f284e7fe623caa73e1 100644 --- a/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -55,6 +55,7 @@ "Accounts_OAuth_Custom_id": "ID", "Accounts_OAuth_Custom_Identity_Path": "Identitätspfad", "Accounts_OAuth_Custom_Login_Style": "Anmeldungsart", + "Accounts_OAuth_Custom_Merge_Users": "BenutzerInnen zusammenführen", "Accounts_OAuth_Custom_Scope": "Bereich", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Token_Path": "Pfad des Token", @@ -118,12 +119,12 @@ "Add_agent": "Agent hinzufügen", "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_manager": "Manager hinzufügen", - "Add_user": "Benutzer hinzufügen", - "Add_User": "Benutzer hinzufügen", - "Add_users": "Benutzer hinzufügen", + "Add_user": "BenutzerIn hinzufügen", + "Add_User": "BenutzerIn hinzufügen", + "Add_users": "BenutzerInnen hinzufügen", "Adding_OAuth_Services": "Hinzufügen von OAuth-Services", "Adding_permission": "Berechtigung hinzufügen", - "Adding_user": "Benutzer hinzufügen", + "Adding_user": "Füge BenutzerIn hinzu", "Additional_emails": "Zusätzliche E-Mails", "Additional_Feedback": "Zusätzliches Feedback", "Administration": "Administration", @@ -205,6 +206,7 @@ "Back_to_integrations": "Zurück zu Integrationen", "Back_to_login": "Zurück zum Login", "Back_to_permissions": "Zurück zu den Berechtigungen", + "Block_User": "BenutzerIn sperren", "Body": "Body", "bold": "fett", "Branch": "Branch", @@ -328,7 +330,7 @@ "Edit_Department": "Abteilung bearbeiten", "edited": "bearbeitet", "Editing_room": "Raum bearbeiten", - "Editing_user": "Benutzer bearbeiten", + "Editing_user": "Benutzern bearbeiten", "Email": "E-Mail", "Email_address_to_send_offline_messages": "E-Mail-Adresse zum Senden von Offline-Nachrichten", "Email_already_exists": "Die E-Mail-Adresse existiert bereits.", @@ -461,6 +463,7 @@ "Force_SSL": "SSL erzwingen", "Force_SSL_Description": "*Achtung!* _Force SSL_ solte niemals mit einem Reverse-Proxy verwendet werden. Falls Sie einen Reverse-Proxy verwenden, sollten Sie die Weiterleitung DORT einrichten. Dies Option existiert für Anwendungen wie Heroku, die keine Weiterleitungskonfigurationen für Reverse-Proxy erlauben.", "Forgot_password": "Passwort vergessen?", + "Forward_to_user": "An BenutzerIn weiterleiten", "Frequently_Used": "Häufig verwendet", "From": "Absender", "From_Email": "Absender", @@ -523,6 +526,9 @@ "Integration_added": "Die Integration wurde hinzugefügt.", "Integration_Incoming_WebHook": "Eingehende WebHook-Integration", "Integration_New": "Neue Integration", + "Integrations_Outgoing_Type_RoomJoined": "BenutzerIn hat den Raum betreten", + "Integrations_Outgoing_Type_RoomLeft": "BenutzerIn hat den Raum verlassen", + "Integrations_Outgoing_Type_UserCreated": "BenutzerIn angelegt.", "Integration_Outgoing_WebHook": "Ausgehende WebHook-Integration", "Integration_Word_Trigger_Placement_Description": "Soll das auslösende Wort irgendwo im Satz stehen und nicht nur am Anfang? ", "Integration_updated": "Die Integration wurde aktualisiert.\n", @@ -548,7 +554,7 @@ "Invitation_Subject": "Einladungsbetreff", "Invitation_Subject_Default": "Sie wurden zu [Site_Name] eingeladen", "Invite_user_to_join_channel": "Benutzer in diesen Raum einladen", - "Invite_Users": "Benutzer einladen", + "Invite_Users": "BenutzerInnen einladen", "is_also_typing": "schreibt auch", "is_also_typing_female": "schreibt auch", "is_also_typing_male": "schreibt auch", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 24e7b29e401dcc777e1c19727e05da759fc595ea..8503b587ac362ad28d73b6c7bd0a3d3d56e267d9 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -17,6 +17,8 @@ "Accessing_permissions": "Zugriff auf Berechtigungen", "Account_SID": "Konto-SID", "Accounts": "Konten", + "Accounts_AllowAnonymousRead": "Erlaube Anonymes lesen", + "Accounts_AllowAnonymousWrite": "Erlaube Anonymes schreiben", "Accounts_AllowDeleteOwnAccount": "Benutzern erlauben, ihr Konto zu löschen", "Accounts_AllowedDomainsList": "Liste von erlaubten Domains", "Accounts_AllowedDomainsList_Description": "Durch Kommata getrennte Liste von erlaubten Domains", @@ -103,6 +105,8 @@ "Accounts_OAuth_Wordpress_id": "WordPress-ID", "Accounts_OAuth_Wordpress_secret": "Geheimer WordPress Schlüssel", "Accounts_PasswordReset": "Passwort zurücksetzen", + "Accounts_OAuth_Proxy_host": "Proxy Host", + "Accounts_OAuth_Proxy_services": "Proxy Service", "Accounts_Registration_AuthenticationServices_Default_Roles": "Standardrolle für Authentifizierungsdienste", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "Standardrollen die Benutzern zugewiesen werden, die sich über Authentifizierungsdienste registrieren", "Accounts_Registration_AuthenticationServices_Enabled": "Anmeldung mit Authentifizierungsdiensten", @@ -130,12 +134,12 @@ "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_Domain": "Domain hinzufügen", "Add_manager": "Manager hinzufügen", - "Add_user": "Benutzer hinzufügen", - "Add_User": "Benutzer hinzufügen", - "Add_users": "Benutzer hinzufügen", + "Add_user": "BenutzerIn hinzufügen", + "Add_User": "BenutzerIn hinzufügen", + "Add_users": "BenutzerInnen hinzufügen", "Adding_OAuth_Services": "Hinzufügen von OAuth-Services", "Adding_permission": "Berechtigung hinzufügen", - "Adding_user": "Benutzer hinzufügen", + "Adding_user": "Füge BenutzerIn hinzu", "Additional_emails": "Zusätzliche E-Mails", "Additional_Feedback": "Zusätzliches Feedback", "Administration": "Administration", @@ -175,6 +179,7 @@ "API_Drupal_URL_Description": "Beispiel: https://domain.de (ohne schließenden /)", "API_Embed": "Einbetten", "API_Embed_Description": "Eingebettete Link Vorschau für Links die von Benutzern gepostet wurden aktiv.", + "API_EmbedCacheExpirationDays": "Tage bis zum Ablauf den eingebetteten Caches", "API_EmbedDisabledFor": "Einbettungen für Benutzer deaktivieren", "API_EmbedDisabledFor_Description": "Durch Kommata getrennte Liste von Benutzernamen", "API_EmbedIgnoredHosts": "Ignorierte Hosts einbetten", @@ -182,9 +187,11 @@ "API_EmbedSafePorts": "Sichere Ports", "API_EmbedSafePorts_Description": "Kommagetrennte Liste der Ports, für die eine Vorschau erlaubt ist.", "API_Enable_CORS": "Aktiviere CORS", + "API_Enable_Direct_Message_History_EndPoint": "Aktiviere den Endpunkt für den Verlauf von Direkt Nachrichten", "API_GitHub_Enterprise_URL": "Server-URL", "API_GitHub_Enterprise_URL_Description": "Beispiel: http://domain.com (ohne Schrägstrich am Ende)", "API_Gitlab_URL": "GitLab-URL", + "API_Shield_Types": "Shield Typ", "API_Token": "API-Token", "API_Upper_Count_Limit": "Max. Anzahl an Einträgen", "API_Upper_Count_Limit_Description": "Max. Anzahl an Einträgen die die REST API zurückliefen soll (sofern ohne Einschränkung)? ", @@ -205,6 +212,7 @@ "Assign_admin": "Admin zuweisen", "at": "am", "Attachment_File_Uploaded": "Datei hochgeladen", + "Attribute_handling": "Behandlung von Attributen", "Auth_Token": "Auth-Token", "Author": "Autor", "Authorization_URL": "Autorisierungs-URL", @@ -237,8 +245,10 @@ "Back": "Zurück", "Back_to_applications": "Zurück zu den Anwendungen", "Back_to_integrations": "Zurück zu Integrationen", + "Back_to_integration_detail": "Zurück zu den Integrations Details", "Back_to_login": "Zurück zum Login", "Back_to_permissions": "Zurück zu den Berechtigungen", + "Block_User": "BenutzerIn sperren", "Body": "Body", "bold": "fett", "bot_request": "Bot-Anfrage", @@ -257,10 +267,13 @@ "Cancel": "Abbrechen", "Cancel_message_input": "Abbrechen", "Cannot_invite_users_to_direct_rooms": "Benutzer können nicht in private Nachrichtenräume eingeladen werden.", + "CAS_autoclose": "Login Pupp automatisch schließen", + "CAS_base_url_Description": "Haupt URL des externen Singe Sign On Services e.g: https://sso.example.undef/sso/", "CAS_button_color": "Hintergrundfarbe des Login-Buttons", "CAS_button_label_color": "Farbe des Login-Button-Texts", "CAS_button_label_text": "Text des Login-Buttons", "CAS_enabled": "Aktiviert", + "CAS_login_url_Description": "Login URL des externen Singe Sign On Services e.g: https://sso.example.undef/sso/login", "CAS_popup_height": "Höhe des Login Pop Up", "CAS_popup_width": "Breite des Login Pop Up", "CAS_Sync_User_Data_Enabled": "Benutzerdaten immer synchronisieren", @@ -291,8 +304,10 @@ "Choose_the_username_that_this_integration_will_post_as": "Wählen Sie den Benutzernamen, der die Integration veröffentlicht.", "clear": "löschen", "clear_cache_now": "Zwischenspeicher jetzt leeren", + "clear_history": "Verlauf löschen", "Clear_all_unreads_question": "Möchten Sie alle ungelesenen Nachrichten löschen?", "Click_here": "Hier klicken", + "Click_here_for_more_info": "Hier klicken für weitere Informationen", "Client_ID": "Client-ID", "Client_Secret": "Client-Schlüssel", "Clients_will_refresh_in_a_few_seconds": "Clients werden in wenigen Sekunden aktualisiert", @@ -336,15 +351,23 @@ "Custom_Fields": "Benutzerdefinierte Felder", "Custom_oauth_helper": "Bei der Einrichtung muss eine Rückruf-URL angegeben werden. Benutze dafür folgende URL: <pre>%s</pre>", "Custom_oauth_unique_name": "Name des OAuth-Kontos", + "Custom_Scripts": "Benutzerdefinierte Skripte", "Custom_Script_Logged_In": "Benutzerdefiniertes Script für angemeldete Benutzer", "Custom_Script_Logged_Out": "Benutzerdefiniertes Script für abgemeldete Benutzer", + "Custom_Sounds": "Benutzerdefinierte Töne", + "Custom_Sound_Add": "Benutzerdefinierte Töne hinzufügen", + "Custom_Sound_Delete_Warning": "Ein gelöschter Ton kann nicht wiederhergestellt werden.", + "Custom_Sound_Error_Invalid_Sound": "Fehlerhafter Ton", "Custom_Translations": "Benutzerdefinierte Ãœbersetzungen", "Dashboard": "Dashboard", "Date": "Datum", + "Date_From": "von", + "Date_to": "bis", "days": "Tage", "DB_Migration": "Datenbankmigration", "DB_Migration_Date": "Datenbankmigrationsdatum", "Deactivate": "Deaktivieren", + "Decline": "ablehnen", "Default": "Voreinstellung", "Delete": "Löschen", "Delete_message": "Nachricht löschen", @@ -366,6 +389,8 @@ "Desktop_Notifications_Enabled": "Desktop-Benachrichtigungen sind aktiviert.", "Direct_message_someone": "Jemandem eine private Nachricht schicken", "Direct_Messages": "Private Nachrichten", + "Disable_Notifications": "Benachrichtigungen deaktivieren", + "Disable_two-factor_authentication": "2 Faktor Authentifizierung deaktivieren", "Display_offline_form": "Offline Nachricht anzeigen", "Displays_action_text": "Zeigt Aktionstext", "Do_you_want_to_change_to_s_question": "Möchten Sie dies zu <strong>%s</strong> ändern?", @@ -384,7 +409,7 @@ "Edit_Department": "Abteilung bearbeiten", "edited": "bearbeitet", "Editing_room": "Raum bearbeiten", - "Editing_user": "Benutzer bearbeiten", + "Editing_user": "BenutzerIn bearbeiten", "Email": "E-Mail", "Email_address_to_send_offline_messages": "E-Mail-Adresse zum Senden von Offline-Nachrichten", "Email_already_exists": "Die E-Mail-Adresse existiert bereits.", @@ -403,7 +428,9 @@ "Empty_title": "Es wurde kein Titel angegeben.", "Enable": "Aktivieren", "Enable_Desktop_Notifications": "Aktivieren", + "Enable_two-factor_authentication": "2-Faktor Authentifizierung aktivieren", "Enabled": "Aktiviert", + "Enable_Svg_Favicon": "SVG Favicon aktivieren", "Encrypted_message": "Verschlüsselte Nachricht", "End_OTR": "OTR beenden", "Enter_a_regex": "Regex eingeben", @@ -437,9 +464,11 @@ "error-invalid-channel-start-with-chars": "Ungültiger Kanal. Beginnen Sie mit @ oder #", "error-invalid-custom-field": "Ungültiges benutzerdefiniertes Feld", "error-invalid-custom-field-name": "Unzulässiger Name für ein benutzerdefiniertes Feld. Benutze nur Buchstaben, Nummern, Binde- und Unterstriche.", + "error-invalid-date": "Das eingegebene Datum ist fehlerhaft.", "error-invalid-description": "Ungültige Beschreibung", "error-invalid-domain": "Ungültige Domain", "error-invalid-email": "Ungültige E-Mail-Adresse: __email__", + "error-invalid-email-address": "Fehlerhafte E-Mail-Adresse", "error-invalid-file-height": "Ungültige Dateihöhe", "error-invalid-file-type": "Ungültiges Dateiformat", "error-direct-message-file-upload-not-allowed": "Dateiaustausch ist in direkten Nachrichten nicht möglich.", @@ -499,8 +528,10 @@ "File_exceeds_allowed_size_of_bytes": "Die Datei ist größer als das erlaubte Maximum von __size__ Bytes", "File_not_allowed_direct_messages": "Dateiaustausch ist in direkten Nachrichten nicht möglich.", "File_type_is_not_accepted": "Feldtyp nicht akzeptiert.", + "File_uploaded": "Datei hochladen", "FileUpload": "Dateien hochladen", "FileUpload_Enabled": "Hochladen von Dateien aktivieren", + "FileUpload_Disabled": "Datei Uploads ", "FileUpload_Enabled_Direct": "Dateiaustausch ist in direkten Nachrichten möglich.", "FileUpload_File_Empty": "Datei ist leer", "FileUpload_FileSystemPath": "Systempfad", @@ -524,17 +555,22 @@ "Follow_social_profiles": "Folge uns in sozialen Netzwerken, fork uns auf GitHub und teile deine Gedanken über die Rocket.Chat-App auf unserem Trello-Board.", "Food_and_Drink": "Essen & Trinken", "Footer": "Fußzeile", + "Fonts": "Schriften", "For_your_security_you_must_enter_your_current_password_to_continue": "Geben Sie zu Ihrer Sicherheit Ihr aktuelles Passwort ein um fortzufahren.", "Force_SSL": "SSL erzwingen", "Force_SSL_Description": "*Achtung!* _Force SSL_ solte niemals mit einem Reverse-Proxy verwendet werden. Falls Sie einen Reverse-Proxy verwenden, sollten Sie die Weiterleitung DORT einrichten. Dies Option existiert für Anwendungen wie Heroku, die keine Weiterleitungskonfigurationen für Reverse-Proxy erlauben.", "Forgot_password": "Passwort vergessen?", + "Forgot_Password_Email_Subject": "[Site_Name] - Passwort Wiederherstellung", + "Forgot_Password_Email": "<a href=\"[Forgot_Password_Url]\">Hier</a> Klicken um das Passwort zurückzusetzen.", + "Forgot_password_section": "Passwort vergessen", "Forward": "Weiterleiten", "Forward_chat": "Chat weiterleiten", "Forward_to_department": "An Abteilung weiterleiten", - "Forward_to_user": "An Benutzer weiterleiten", + "Forward_to_user": "An BenutzerIn weiterleiten", "Frequently_Used": "Häufig verwendet", + "Friday": "Freitag", "From": "Absender", - "From_Email": "Absender", + "From_Email": "E-Mail Absender", "From_email_warning": "<b>Warnung</b>: Der <b>Absender</b> ist Gegenstand deiner Mail-Server-Einstellungen.", "General": "Allgemeines", "github_no_public_email": "Sie haben keine öffentliche E-Mail-Adresse in Ihrem GitHub-Account.", @@ -545,6 +581,7 @@ "Guest_Pool": "Gästepool", "Hash": "Hash", "Header": "Kopfzeile", + "Header_and_Footer": "Kopf und Fusszeile", "Hidden": "Versteckt", "Hide_Avatars": "Avatar verstecken", "Hide_flextab": "Rechte Seitenleiste über Klick verstecken", @@ -552,6 +589,7 @@ "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verstecken?", "Hide_room": "Raum verstecken", "Hide_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verstecken?", + "Hide_roles": "Rollen verstecken", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", "Highlights_How_To": "Um benachrichtigt zu werden, wenn ein Wort oder Ausdruck erwähnt wird, fügen Sie ihn hier hinzu. Sie können Wörter und Ausdrücke mit Kommata trennen. Die Wörter zur Hervorhebung beachten die Groß- und Kleinschreibung nicht.", @@ -598,9 +636,17 @@ "Installation": "Installation", "Installed_at": "Installationsdatum", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Anweisungen an Ihre Besucher: Füllen Sie das Formular aus, um eine Nachricht zu senden.", + "Integration_Advanced_Settings": "Erweiterte Einstellungen", "Integration_added": "Die Integration wurde hinzugefügt.", "Integration_Incoming_WebHook": "Eingehende WebHook-Integration", "Integration_New": "Neue Integration", + "Integrations_Outgoing_Type_FileUploaded": "Hochgeladene Dateien", + "Integrations_Outgoing_Type_RoomArchived": "Archivierte Räume", + "Integrations_Outgoing_Type_RoomCreated": "Raum erstellt (öffentlich und privat)", + "Integrations_Outgoing_Type_RoomJoined": "BenutzerIn hat den Raum betreten", + "Integrations_Outgoing_Type_RoomLeft": "BenutzerIn hat den Raum verlassen", + "Integrations_Outgoing_Type_SendMessage": "Nachricht gesendet", + "Integrations_Outgoing_Type_UserCreated": "BenutzerIn angelegt.", "Integration_Outgoing_WebHook": "Ausgehende WebHook-Integration", "Integration_Word_Trigger_Placement_Description": "Soll das auslösende Wort irgendwo im Satz stehen und nicht nur am Anfang? ", "Integration_updated": "Die Integration wurde aktualisiert.\n", @@ -618,15 +664,17 @@ "Invalid_pass": "Es muss ein Passwort angegeben werden.", "Invalid_room_name": "<strong>%s</strong> ist kein zulässiger Raumname.<br/> Verwenden Sie nur Buchstaben, Zahlen oder Binde- und Unterstriche.", "Invalid_secret_URL_message": "Die angegebene URL ist ungültig.", + "Invalid_two_factor_code": "Fehlerhafter 2-Faktor Code", "invisible": "unsichtbar", "Invisible": "Unsichtbar", + "Invitation": "Einladung", "Invitation_HTML": "Einladungstext (HTML)", "Invitation_HTML_Default": "<h2> Sie wurden eingeladen zu <h1> [Site_Name] </h1></h2><p> Besuchen Sie zu [Site_URL] und probieren Sie heute die beste verfügbare Open-Source-Chat-Lösung aus! </p>", "Invitation_HTML_Description": "Sie können die folgenden Platzhalter verwenden: <br /><ul><li> [email] für den Empfänger der E-Mail. </li><li> [Site_Name] und [Site_URL] jeweils für den Anwendungsnamen und die URL. </li></ul>", "Invitation_Subject": "Einladungsbetreff", "Invitation_Subject_Default": "Sie wurden zu [Site_Name] eingeladen", - "Invite_user_to_join_channel": "Benutzer in diesen Kanal einladen", - "Invite_Users": "Benutzer einladen", + "Invite_user_to_join_channel": "BenutzerIn in diesen Kanal einladen", + "Invite_Users": "BenutzerInnen einladen", "is_also_typing": "schreibt auch", "is_also_typing_female": "schreibt auch", "is_also_typing_male": "schreibt auch", @@ -639,6 +687,7 @@ "Jitsi_Enable_Channels": "Aktivieren in Kanälen", "join": "Beitreten", "Join_audio_call": "Anruf beitreiten", + "Join_Chat": "Chat beitreten", "Join_default_channels": "Standardkanälen beitreten", "Join_the_Community": "Trete der Community bei", "Join_the_given_channel": "Diesem Kanal beitreten", @@ -718,6 +767,7 @@ "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verlassen?", "Leave_room": "Raum verlassen", "Leave_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verlassen?", + "Leave_the_current_channel": "Aktuellen Kanal verlassen", "line": "Zeilen", "List_of_Channels": "Liste der Kanäle", "List_of_Direct_Messages": "Liste der Direktnachrichten", @@ -810,7 +860,9 @@ "Message_ShowFormattingTips": "Formatierungstipps anzeigen", "Message_starring": "Markieren von Nachrichten", "Message_TimeFormat": "Zeitformat", + "Message_TimeAndDateFormat": "Zeit und Datumsformat", "Message_TimeFormat_Description": "Siehe auch: <a href=\"http://momentjs.com/docs/#/displaying/format/\" target=\"momemt\">Moment.js</a>", + "Message_TimeAndDateFormat_Description": "Siehe auch: <a href=\"http://momentjs.com/docs/#/displaying/format/\" target=\"momemt\">Moment.js</a>", "Message_too_long": "Diese Nachricht ist zu lang.", "Message_VideoRecorderEnabled": "Videoaufnahmen eingeschaltet", "Message_VideoRecorderEnabledDescription": "Videoformat auf webm beim \"Datei hochladen\" einschränken? (Video abspielen funktioniert dann in fast allen Browsern)", @@ -818,11 +870,14 @@ "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht.", "Meta": "Metadaten", "Meta_fb_app_id": "Facebook-App-ID", + "Meta_custom": "Benutzerdefinierte Meta Tags", "Meta_google-site-verification": "Google-Seiten-Verifizierung", "Meta_language": "Sprache", "Meta_msvalidate01": "MSValidate.01", "Meta_robots": "Roboter", + "Min_length_is": "Die minimale länge beträgt %s", "minutes": "Minuten", + "Mobile": "Mobil", "Monday": "Montag", "Monitor_history_for_changes_on": "Verlaufsänderungen beobachten für", "More_channels": "Mehr Kanäle", @@ -886,16 +941,19 @@ "Off_the_record_conversation_is_not_available_for_your_browser_or_device": "Off-the-record-Gespräche sind für Ihren Browser oder Ihr Gerät nicht verfügbar.", "Office_Hours": "Bürozeiten", "Office_hours_enabled": "Bürozeiten aktiviert", + "Office_hours_updated": "Bürozeiten aktualisiert", "Offline": "Offline", "Offline_DM_Email": "Sie haben eine private Nachricht von __user__ erhalten.", "Offline_form": "Offline-Formular", "Offline_form_unavailable_message": "Nachricht, dass Offline Formular ungültig", + "Offline_Link_Message": "gehe zur Nachricht", "Offline_Mention_Email": "Sie wurden von __user__ in #__room__ erwähnt.", "Offline_message": "Offline-Nachricht", "Offline_success_message": "Nachricht, dass Offline Nachricht erfolgreich", "Offline_unavailable": "offline - nicht verfügbar", "On": "Ein", "Online": "Online", + "Only_On_Desktop": "Desktop Modus (senden mit der \"Enter\" Taste nur auf dem Desktop PC)", "Only_you_can_see_this_message": "Nur Sie können diese Nachricht sehen.", "Oops!": "Hoppla", "Open": "Öffnen", @@ -941,10 +999,12 @@ "Please_enter_value_for_url": "Bitte geben Sie eine URL für Ihr Profilbild ein.", "Please_enter_your_new_password_below": "Bitte geben Sie Ihr neues Passwort ein:", "Please_enter_your_password": "Bitte Passwort eingeben", + "please_enter_valid_domain": "Bitte eine valide Domain eingeben", "Please_fill_a_label": "Bitte Bezeichnung ausfüllen", "Please_fill_a_name": "Bitte geben Sie einen Namen ein.", "Please_fill_a_username": "Bitte geben Sie einen Benutzernamen ein.", "Please_fill_name_and_email": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.", + "Please_select_an_user": "Bitte einen Benutzer auswählen", "Please_select_enabled_yes_or_no": "Bitte wählen Sie eine Option für \"aktiviert\".", "Please_wait": "Bitte warten", "Please_wait_activation": "Bitte warten, der Vorgang kann einige Zeit in Anspruch nehmen.", @@ -988,14 +1048,23 @@ "quote": "Zitat", "Quote": "Zitieren", "Random": "Zufällig", + "React_when_read_only": "Reaktionen erlauben", "Reacted_with": "Reagierte mit", "Reactions": "Reaktionen", + "Read_only": "Nur lesend", + "Read_only_channel": "Kanal nur lesbar", + "Read_only_group": "Nur lesbar Gruppe", "Record": "Aufnehmen", "Redirect_URI": "Weiterleitungs-URL", + "Refresh_oauth_services": "oAuth Service aktualisieren", "Refresh_keys": "Schlüssel aktualisieren", "Refresh_your_page_after_install_to_enable_screen_sharing": "Aktualisieren Sie die Seite nach der Installation, um die Bildschirmübertragung zu aktivieren.", + "Regenerate_codes": "Codes neu generieren", "Register": "Neues Konto registrieren", + "Registration": "Registrierung", "Registration_Succeeded": "Ihre Registrierung war erfolgreich.", + "Registration_via_Admin": "Registrierung via Admin", + "Regular_Expressions": "Reguläre Ausdrücke", "Release": "Veröffentlichung", "Remove": "Entfernen", "Remove_Admin": "Admin entfernen", @@ -1003,8 +1072,10 @@ "Remove_as_owner": "als Besitzer entfernen", "Remove_custom_oauth": "OAuth-Konto entfernen", "Remove_from_room": "Aus dem Raum entfernen", + "Remove_last_admin": "Entferne den letzen Admin", "Remove_someone_from_room": "Jemanden aus dem Raum entfernen", "Removed": "Entfernt", + "Reply": "Antwort", "Report_Abuse": "Missbrauch melden", "Report_exclamation_mark": "Melden!", "Report_sent": "Bericht gesendet", @@ -1028,10 +1099,16 @@ "room_changed_topic": "Das Thema des Raums wurde von <em>__user_by__</em> zu <em>__room_topic__</em> geändert.", "Room_description_changed_successfully": "Raumbeschreibung erfolgreich geändert", "Room_has_been_deleted": "Der Raum wurde gelöscht.", + "Room_has_been_archived": "Der Raum wurde archiviert.", + "Room_has_been_unarchived": "Der Raum wurde dearchiviert.", "Room_Info": "Raum", + "room_is_blocked": "Der räum ist geblockt", + "room_is_read_only": "Der Raum ist nur lesbar", + "room_name": "Raum Name", "Room_name_changed": "<em>__user_by__</em> hat den Raumnamen zu <em>__room_name__</em> geändert.", "Room_name_changed_successfully": "Der Raumname wurde erfolgreich geändert.", "Room_not_found": "Der Raum konnte nicht gefunden werden.", + "Room_password_changed_successfully": "Das Raum Passwort wurde erfolgreich geändert", "Room_topic_changed_successfully": "Das Thema des Raums wurde erfolgreich geändert.", "Room_type_changed_successfully": "Der Raumtyp wurde erfolgreich geändert.", "Room_unarchived": "Der Raum wurde wiederhergestellt.", @@ -1046,6 +1123,8 @@ "SAML_Custom_Generate_Username": "Benutzernamen generieren", "SAML_Custom_Issuer": "Benutzerdefinierter Aussteller", "SAML_Custom_Provider": "Benutzerdefinierter Provider", + "SAML_Custom_Public_Cert": "Öffentliches Zertifikat", + "SAML_Custom_Private_Key": "Privater Schlüssel", "Saturday": "Samstag", "Save": "Speichern", "Save_changes": "Änderungen speichern", @@ -1099,6 +1178,7 @@ "Show_all": "Alle Nutzer zeigen", "Show_more": "Mehr Nutzer zeigen", "show_offline_users": "Zeige Benutzer an, die offline sind", + "Show_on_registration_page": "Auf der Registrierungsseite anzeigen", "Show_only_online": "Nur Online-Nutzer zeigen", "Show_preregistration_form": "Vorregistrierungsformular zeigen", "Showing_archived_results": "<b>%s</b> archivierte Räume", @@ -1117,6 +1197,7 @@ "Slash_Topic_Description": "Thema setzen", "Slash_Topic_Params": "Themennachricht", "Smarsh_Enabled": "Smarsh aktiviert", + "Smarsh_MissingEmail_Email": "Fehlende E-Mail", "Smileys_and_People": "Gesichter & Personen", "SMS_Enabled": "SMS aktiviert", "SMTP": "SMTP", @@ -1125,6 +1206,7 @@ "SMTP_Port": "SMTP-Port", "SMTP_Test_Button": "SMTP-Einstellungen testen", "SMTP_Username": "SMTP-Benutzername", + "Snippet_Added": "Erstellt am %s", "Sound": "Ton", "SSL": "SSL", "Star_Message": "Nachricht markieren", @@ -1165,7 +1247,9 @@ "Symbols": "Symbole", "Sync_success": "Die Synchronisierung war erfolgreich.", "Sync_Users": "Benutzer synchronisieren", + "System_messages": "System Nachrichten", "Tag": "Tag", + "TargetRoom": "Ziel Raum!", "Test_Connection": "Testverbindung", "Test_Desktop_Notifications": "Desktop-Benachrichtigungen testen", "Thank_you_exclamation_mark": "Vielen Dank!", @@ -1200,11 +1284,16 @@ "There_are_no_agents_added_to_this_department_yet": "Es wurden bisher keine Agenten zu dieser Abteilung hinzugefügt.", "There_are_no_integrations": "Es sind keine Integrationen vorhanden.", "There_are_no_users_in_this_role": "Es sind dieser Rolle keine Benutzer zugeordnet.", + "This_conversation_is_already_closed": "Die Unterhaltung wurde bereits beendet.", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "Diese E-Mail wurde bereits verschickt, aber noch nicht bestätigt. Bitte ändern Sie Ihr Passwort.", "This_is_a_desktop_notification": "Das ist eine Desktop-Benachrichtigung.", "This_is_a_push_test_messsage": "Dies ist eine Test-Push-Nachricht.", "This_room_has_been_archived_by__username_": "Dieser Raum wurde von __username__ archiviert", "This_room_has_been_unarchived_by__username_": "Dieser Raum wurde von __username__ unarchiviert", + "Two-factor_authentication": "2-Faktor Authentifizierung", + "Two-factor_authentication_disabled": "2-Faktor Authentifizierung deaktiviert", + "Two-factor_authentication_enabled": "2-Faktor Authentifizierung aktiviert", + "Two-factor_authentication_is_currently_disabled": "2-Faktor Authentifizierung ist momentan deaktiviert", "Thursday": "Donnerstag", "Time_in_seconds": "Zeit in Sekunden", "Title": "Titel", @@ -1216,6 +1305,8 @@ "To_users": "An die Benutzer", "Topic": "Thema", "Travel_and_Places": "Reisen & Orte", + "Translated": "übersetzt", + "Translations": "Ãœbersetzungen", "Trigger_removed": "Auslöser entfernt", "Trigger_Words": "Trigger Words", "Triggers": "Auslöser", @@ -1228,7 +1319,9 @@ "Type_your_new_password": "Geben Sie Ihr neues Passwort ein", "UI_DisplayRoles": "Rollen anzeigen", "UI_Merge_Channels_Groups": "Führe private Gruppen und Kanäle zusammen", + "UI_Use_Real_Name": "Benutze den Realen Namen", "Unarchive": "Wiederherstellen", + "Unblock_User": "Benutzer entsperren", "Unmute_someone_in_room": "Jemanden das Chatten in einem Raum wieder erlauben", "Unmute_user": "Benutzern das Chatten erlauben ", "Unnamed": "Unbenannt", @@ -1273,6 +1366,7 @@ "User_left_male": "Der Benutzer <em>__user_left__</em> hat den Kanal verlassen.", "User_logged_out": "Der Benutzer wurde abgemeldet.", "User_management": "Benutzerverwaltung", + "User_muted": "Benutzer stummgeschaltet", "User_muted_by": "Dem Benutzer <em>__user_muted__</em> wurde das Chatten von <em>__user_by__</em> verboten.", "User_not_found": "Der Benutzer konnte nicht gefunden werden.", "User_not_found_or_incorrect_password": "Entweder konnte der Benutzer nicht gefunden werden oder Sie haben ein falsches Passwort angegeben.", @@ -1297,15 +1391,23 @@ "Username_title": "Benutzernamen festlegen", "Username_wants_to_start_otr_Do_you_want_to_accept": "__username__ möchte ein OTR-Gespräch starten. Möchten Sie annehmen?", "Users": "Benutzer", + "Users_added": "Die Benutzer wurden hinzugefügt", "Users_in_role": "Zugeordnete Nutzer", "UTF8_Names_Slugify": "UTF8-Namen-Slugify", "UTF8_Names_Validation": "UTF8-Namen-Verifizierung", "UTF8_Names_Validation_Description": "Erlauben Sie keine Sonderzeichen und Leerzeichen. Sie können - _ und . verwenden, aber nicht am Ende eines Namens.", + "Validate_email_address": "E-Mail-Adresse bestätigen", + "Verification": "Ãœberprüfung ", "Verification_email_sent": "Bestätigungsmail gesendet", + "Verification_Email_Subject": "[Site_Name] - Bestätige dein Benutzerkonto", + "Verification_Email": "Klicke <a href=\"[Verification_Url]\">hier</a> um dein Benutzerkonto zu bestätigen.", "Verified": "Verifiziert", + "Verify": "überprüfen", "Version": "Version", "Video_Chat_Window": "Video-Chat", + "Video_Conference": "Video-Konferenz", "Videocall_declined": "Videoanruf abgelehnt.", + "Videocall_enabled": "Videoanruf aktiviert", "View_All": "Alle ansehen", "View_Logs": "Logs anzeigen", "View_mode": "Ansichts-Modus", @@ -1319,6 +1421,7 @@ "Visitor_page_URL": "URL der Besucherseite", "Visitor_time_on_site": "Besucherzeit auf der Seite", "Wait_activation_warning": "Bevor Sie sich anmelden können, muss das Konto von einem Administrator manuell aktiviert werden.", + "Warnings": "Warnungen", "We_are_offline_Sorry_for_the_inconvenience": "Wir sind offline. Entschuldigen Sie die Unannehmlichkeiten.", "We_have_sent_password_email": "Wir haben Ihnen eine Anleitung zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Wenn Sie keine E-Mail erhalten haben, versuchen Sie es bitte noch einmal.", "We_have_sent_registration_email": "Wir haben Ihnen eine Bestätigungsmail gesendet. Wenn Sie keine E-Mail erhalten haben, versuchen Sie es bitte noch einmal.", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 1d18d31ef92382a2ac30d5dd485734ed7c9739f6..0be9c641d9cff4742e049ab1dceed31fc3ec5c9f 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -267,6 +267,7 @@ "BotHelpers_userFields": "User Fields", "BotHelpers_userFields_Description": "CSV of user fields that can be accessed by bots helper methods.", "Branch": "Branch", + "Broadcast_Connected_Instances": "Broadcast Connected Instances", "Bugsnag_api_key": "Bugsnag API Key", "busy": "busy", "Busy": "Busy", @@ -362,6 +363,7 @@ "CROWD_Reject_Unauthorized": "Reject Unauthorized", "CRM_Integration": "CRM Integration", "Current_Chats": "Current Chats", + "Current_Status": "Current Status", "Custom": "Custom", "Custom_Emoji": "Custom Emoji", "Custom_Emoji_Add": "Add New Emoji", @@ -706,6 +708,7 @@ "Install_FxOs_follow_instructions": "Please confirm the app installation on your device (press \"Install\" when prompted).", "Installation": "Installation", "Installed_at": "Installed at", + "Instance_Record": "Instance Record", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions to your visitor fill the form to send a message", "Impersonate_user": "Impersonate User", "Impersonate_user_description": "When enabled, integration posts as the user that triggered integration", @@ -1256,6 +1259,7 @@ "Reset_password": "Reset password", "Restart": "Restart", "Restart_the_server": "Restart the server", + "Retry_Count": "Retry Count", "Role": "Role", "Role_Editing": "Role Editing", "Role_removed": "Role removed", @@ -1560,6 +1564,7 @@ "Unread_Rooms": "Unread Rooms", "Unread_Rooms_Mode": "Unread Rooms Mode", "Unstar_Message": "Remove Star", + "Updated_at": "Updated at", "Upload_file_description": "File description", "Upload_file_name": "File name", "Upload_file_question": "Upload file?", diff --git a/packages/rocketchat-i18n/i18n/fr.i18n.json b/packages/rocketchat-i18n/i18n/fr.i18n.json index 7d9b43e0c9fcb5533db0a5d4bffa9cd8a4ecb787..96b29850cf853a08c020201eca7c637cd2d21fce 100644 --- a/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -10,13 +10,15 @@ "__username__is_no_longer__role__defined_by__user_by_": "__user_by__ a retiré le rôle __role__ à __username__", "__username__was_set__role__by__user_by_": "__user_by__ a donné le rôle __role__ à __username__", "Accept": "Accepter", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demande de chat en direct même si il n'y a pas d'assistant en ligne", - "Accept_with_no_online_agents": "Accepter sans assistant enligne", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demandes de chat en ligne même si il n'y a pas d'agent en ligne", + "Accept_with_no_online_agents": "Accepter sans agent en ligne", "Access_not_authorized": "Accès non autorisé", "Access_Token_URL": "URL du jeton d'accès", "Accessing_permissions": "Accès aux permissions", "Account_SID": "SID du compte", "Accounts": "Comptes", + "Accounts_AllowAnonymousRead": "Autoriser la lecture anonyme", + "Accounts_AllowAnonymousWrite": "Autoriser l'écriture anonyme", "Accounts_AllowDeleteOwnAccount": "Autoriser les utilisateurs à supprimer leur propre compte", "Accounts_AllowedDomainsList": "Liste des domaines autorisés", "Accounts_AllowedDomainsList_Description": "Liste des domaines autorisés, séparés par des virgules", @@ -34,6 +36,7 @@ "Accounts_BlockedUsernameList": "Liste des noms d'utilisateurs bloqués", "Accounts_BlockedUsernameList_Description": "Liste de noms d'utilisateurs bloqués (insensible à la casse), séparés par des virgules", "Accounts_CustomFields_Description": "Devrait être un JSON valide où les clés sont les noms des champs contenant un dictionnaire de champs de paramétrage. Exemple :<br/>\n<code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"eleve\",\n  \"options\": [\"enseignant\", \"eleve\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", + "Accounts_DefaultUsernamePrefixSuggestion": "Suggestion par défaut du préfixe du nom d'utilisateur", "Accounts_denyUnverifiedEmail": "Refuser les e-mails non vérifiés", "Accounts_EmailVerification": "Vérification de l'adresse e-mail", "Accounts_EmailVerification_Description": "Vous devez avoir des paramètres SMTP corrects pour utiliser cette fonctionnalité", @@ -137,8 +140,8 @@ "Additional_Feedback": "Commentaires supplémentaires", "Administration": "Administration", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Après l'authentification par OAuth2, les utilisateurs seront redirigés vers cette URL", - "Agent": "Assistant", - "Agent_added": "Assistant ajouté", + "Agent": "Agent", + "Agent_added": "Agent ajouté", "Agent_removed": "Assistant supprimé", "Alias": "Alias", "Alias_Format": "Format d'alias", @@ -377,8 +380,8 @@ "Delete_Room_Warning": "Supprimer un salon supprimera également tous les messages postés dans le salon. Cette action est irréversible.", "Delete_User_Warning": "Supprimer un utilisateur va également supprimer tous les messages de celui-ci. Cette action est irréversible.", "Deleted": "Supprimé !", - "Department": "Département", - "Department_removed": "Département supprimé", + "Department": "Service", + "Department_removed": "Service supprimé", "Departments": "Départements", "Deployment_ID": "ID de déploiement", "Description": "Description", @@ -409,7 +412,7 @@ "Duration": "Durée", "Edit": "Modifier", "Edit_Custom_Field": "Modifier le champ personnalisé", - "Edit_Department": "Éditer le département", + "Edit_Department": "Éditer le service", "Edit_Trigger": "Éditer le déclencheur", "edited": "modifié", "Editing_room": "Modification du salon", @@ -459,7 +462,7 @@ "error-could-not-change-name": "Impossible de modifier le nom", "error-could-not-change-username": "Impossible de modifier le nom d'utilisateur", "error-delete-protected-role": "Impossible de supprimer un rôle protégé", - "error-department-not-found": "Département introuvable", + "error-department-not-found": "Service introuvable", "error-duplicate-channel-name": "Un canal avec le nom '__channel_name__' existe déjà ", "error-email-domain-blacklisted": "Le domaine de l'adresse e-mail est sur liste noire", "error-email-send-failed": "Erreur lors de la tentative d'envoi d'e-mail : __message__", @@ -581,7 +584,7 @@ "Forgot_password_section": "Mot de passe oublié", "Forward": "Transmettre", "Forward_chat": "Transmettre la conversation", - "Forward_to_department": "Transmettre au département", + "Forward_to_department": "Transmettre au service", "Forward_to_user": "Transmettre à l'utilisateur", "Frequently_Used": "Fréquemment utilisé", "Friday": "Vendredi", @@ -614,10 +617,10 @@ "Host": "Hôte", "hours": "heures", "Hours": "Heures", - "How_friendly_was_the_chat_agent": "L'assistant du chat était-il amical ?", - "How_knowledgeable_was_the_chat_agent": "L'assistant du chat était-il clair ?", + "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il amical ?", + "How_knowledgeable_was_the_chat_agent": "Votre interlocuteur était-il clair ?", "How_long_to_wait_after_agent_goes_offline": "Délai d'attente après que l'agent soit hors ligne", - "How_responsive_was_the_chat_agent": "L'assistant du chat avait-il des réponses adaptées ?", + "How_responsive_was_the_chat_agent": "Votre interlocuteur avait-il des réponses adaptées ?", "How_satisfied_were_you_with_this_chat": "Étiez-vous satisfait de ce chat?", "How_to_handle_open_sessions_when_agent_goes_offline": "Comment gérer les sessions ouvertes lorsque l'asistant passe hors ligne", "If_this_email_is_registered": "Si cet e-mail est enregistré, les instructions pour réinitialiser votre mot de passe vous serons envoyées. Si vous ne recevez pas d'email rapidement, merci de revenir et d'essayer à nouveau.", @@ -953,12 +956,12 @@ "N_new_messages": "%s nouveaux messages", "Name": "Nom", "Name_cant_be_empty": "Le nom ne peut pas être vide", - "Name_of_agent": "Nom de l'assistant", + "Name_of_agent": "Nom de l'agent", "Name_optional": "Nom (optionnel)", "Navigation_History": "Historique de navigation", "New_Application": "Nouvelle application", "New_Custom_Field": "Nouveau champ personnalisé", - "New_Department": "Nouveau département", + "New_Department": "Nouveau service", "New_integration": "Nouvelle intégration", "New_logs": "Nouveaux journaux", "New_Message_Notification": "Notification de nouveau message", @@ -968,7 +971,7 @@ "New_Room_Notification": "Notification de nouveau salon", "New_videocall_request": "Nouvelle demande d'appel vidéo", "New_Trigger": "Nouveau déclencheur", - "No_available_agents_to_transfer": "Aucun assistant disponible pour le transfert", + "No_available_agents_to_transfer": "Aucun agent disponible pour le transfert", "No_channel_with_name_%s_was_found": "Aucun canal nommé <strong>\"%s\"</strong> n'a été trouvé !", "No_channels_yet": "Vous ne faites partie d’aucun canal pour le moment.", "No_direct_messages_yet": "Vous n'avez pris part à aucune discussion pour le moment.", @@ -1029,6 +1032,7 @@ "optional": "facultatif", "or": "ou", "Order": "Ordre", + "Or_talk_as_anonymous": "Ou discutez de manière anonyme", "OS_Arch": "Architecture", "OS_Cpus": "Nombre de CPU", "OS_Freemem": "Mémoire disponible", @@ -1216,7 +1220,7 @@ "seconds": "secondes", "Secret_token": "Jeton secret", "Security": "Sécurité", - "Select_a_department": "Sélectionner un département", + "Select_a_department": "Sélectionner un service", "Select_a_user": "Sélectionner un utilisateur", "Select_an_avatar": "Choisissez un avatar", "Select_file": "Sélectionnez le fichier", @@ -1261,6 +1265,7 @@ "Showing_archived_results": "<p>Affichage de <b>%s</b> résultats archivés</p>", "Showing_online_users": "<b>__total_showing__</b> utilisateur(s) affichés sur un total de __total__", "Showing_results": "<p><b>%s</b> résultat(s)</p>", + "Sign_in_to_start_talking": "Connectez vous pour commencer à discuter", "since_creation": "depuis %s", "Site_Name": "Nom du site", "Site_Url": "URL du site", @@ -1550,7 +1555,7 @@ "Yes_leave_it": "Oui, je veux partir !", "Yes_mute_user": "Oui, rend muet l'utilisateur !", "Yes_remove_user": "Oui, éjecte l'utilisateur !", - "You": "Toi", + "You": "Vous", "you_are_in_preview_mode_of": "Aperçu du salon #<strong>__room_name__</strong> ", "You_are_logged_in_as": "Vous êtes connecté en tant que", "You_are_not_authorized_to_view_this_page": "Vous n'avez pas l'autorisation de voir cette page.", diff --git a/packages/rocketchat-i18n/i18n/ko.i18n.json b/packages/rocketchat-i18n/i18n/ko.i18n.json index 02492e8aa80cd41ca749842fdf2d6630aa591094..c02eee6cb01bfbc2afe97bc3623e8a4dce8561ad 100644 --- a/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -6,17 +6,17 @@ "403": "금지ë¨", "500": "내부 서버 오류", "@username": "@사용ìžëª…", - "@username_message": "@username <message>", - "__username__is_no_longer__role__defined_by__user_by_": "__ ì‚¬ìš©ìž ì´ë¦„ __는 __user_by__ì— ì˜í•´, __role__ ë” ì´ìƒ 없다", - "__username__was_set__role__by__user_by_": "__ ì‚¬ìš©ìž ì´ë¦„ __ì€ __user_by__ì— ì˜í•´ __role__ ì„¤ì •í–ˆë‹¤", + "@username_message": "@사용ìžì´ë¦„ <message>", + "__username__is_no_longer__role__defined_by__user_by_": "__사용ìžëŠ” ë”ì´ìƒ __ê°€ì„¤ì •í•œ __ì—í• ì´ ì—†ìŠµë‹ˆë‹¤", + "__username__was_set__role__by__user_by_": "__ 사용ìžì—게 __사용ìžê°€ __ì—í• ì„__ ì„¤ì •í•˜ì˜€ìŠµë‹ˆë‹¤", "Accept": "수ë½", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "온ë¼ì¸ ìƒë‹´ì›ì´ì—†ëŠ” 경우ì—ë„ ë“¤ì–´ì˜¤ëŠ” 실시간 채팅 ìš”ì² ìˆ˜ë½", - "Access_not_authorized": "액세스 ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "ìƒë‹´ì›ì´ 온ë¼ì¸ ìƒíƒœê°€ ì•„ë‹Œ 경우ì—ë„ ë¼ì´ë¸Œì±—ì„ ìˆ˜ë½í•©ë‹ˆë‹¤.", + "Access_not_authorized": "엑세스 ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤", "Access_Token_URL": "액세스 í† í° URL", - "Accessing_permissions": "권한 액세스", + "Accessing_permissions": "ì ‘ì†ê¶Œí•œ", "Account_SID": "ê³„ì • SID", "Accounts": "ê³„ì •", - "Accounts_AllowDeleteOwnAccount": "사용ìžê°€ ìžì‹ ì˜ ê³„ì •ì„ ì‚ì œí• ìˆ˜ 있습니다.", + "Accounts_AllowDeleteOwnAccount": "사용ìžê°€ ìžì‹ ì˜ ê³„ì •ì„ ì‚ì œí• ìˆ˜ 있습니다", "Accounts_AllowedDomainsList": "í—ˆìš©ëœ ë„ë©”ì¸ ëª©ë¡", "Accounts_AllowedDomainsList_Description": "í—ˆìš©ëœ ë„ë©”ì¸ì„ 쉼표(,)ë¡œ 구분하기", "Accounts_AllowEmailChange": "ì´ë©”ì¼ ë³€ê²½ì„ í—ˆìš©í•©ë‹ˆë‹¤", @@ -29,10 +29,10 @@ "Accounts_AvatarStorePath": "아바타 ì €ìž¥ 경로", "Accounts_AvatarStoreType": "아바파 ì €ìž¥ 타입", "Accounts_BlockedDomainsList": "ì°¨ë‹¨ëœ ë„ë©”ì¸ ëª©ë¡", - "Accounts_BlockedDomainsList_Description": "차단 ëœ ë„ë©”ì¸ì˜ 쉼표로 구분 ëœ ëª©ë¡", - "Accounts_BlockedUsernameList": "차단 ëœ ì‚¬ìš©ìž ì´ë¦„ 목ë¡", - "Accounts_BlockedUsernameList_Description": "차단 ëœ ì‚¬ìš©ìž ì´ë¦„ì˜ ì‰¼í‘œë¡œ 구분 ëœ ëª©ë¡ (대소 ë¬¸ìž êµ¬ë¶„)", - "Accounts_CustomFields_Description": "키는 í•„ë“œ ì„¸íŒ…ì˜ ë”•ì…”ë„ˆë¦¬(dictionary) 를 í¬í•¨í•˜ëŠ” í•„ë“œ ì´ë¦„들ì´ì–´ì•¼ 합니다.\n\n예:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", + "Accounts_BlockedDomainsList_Description": "쉼표로 êµ¬ë¬¸ëœ ì°¨ë‹¨ ë„ë©”ì¸ ë¦¬ìŠ¤íŠ¸", + "Accounts_BlockedUsernameList": "ì°¨ë‹¨ëœ ì‚¬ìš©ìž ë¦¬ìŠ¤íŠ¸", + "Accounts_BlockedUsernameList_Description": "쉼표로 êµ¬ë¬¸ëœ ì°¨ë‹¨ ì‚¬ìš©ìž ë¦¬ìŠ¤íŠ¸ (대소 ë¬¸ìž êµ¬ë¶„)", + "Accounts_CustomFields_Description": "í•„ë“œ ì„¤ì •ì— í¬í•¨ëœ í•„ë“œëª…ì„ ì‚¬ìš©í•œ 올바른 JSON ì´ì—¬ì•¼ 합니다.\n\n예:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", "Accounts_denyUnverifiedEmail": "확ì¸ë˜ì§€ ì•Šì€ ì´ë©”ì¼ ê±°ë¶€", "Accounts_EmailVerification": "ì´ë©”ì¼ í™•ì¸", "Accounts_EmailVerification_Description": "ì´ ê¸°ëŠ¥ì„ ì‚¬ìš©í•˜ë ¤ë©´ SMTPì„¤ì •ì´ ì˜¬ë°”ë¥´ê²Œ ë˜ì–´ìžˆëŠ”지 확ì¸í•´ì£¼ì‹ì‹œì˜¤.", @@ -40,7 +40,7 @@ "Accounts_Enrollment_Email_Default": "<h2> ì— ì˜¤ì‹ ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ <h1> [Site_Name] </h1></h2><p> [Site_URL]ë¡œ ì´ë™í•˜ì—¬ ì˜¤ëŠ˜ë‚ ìµœê³ ì˜ ì˜¤í”ˆ 소스 채팅 솔루션ì„ë³´ì‹ì‹œì˜¤! </p>", "Accounts_Enrollment_Email_Description": "ë‹¹ì‹ ì€ ê°ê° 사용ìžì˜ ì „ì²´ ì´ë¦„, ì´ë¦„ ë˜ëŠ” ì„±ì„ ìœ„í•´ [lname], [name], [fname]ì„ ì‚¬ìš©í• ìˆ˜ 있습니다. <br /> ë‹¹ì‹ ì€ ì‚¬ìš©ìžì˜ ì´ë©”ì¼ì„ [email]ì„ ì‚¬ìš©í• ìˆ˜ 있습니다.", "Accounts_Enrollment_Email_Subject_Default": "[Site_Name] ì— ì˜¤ì‹ ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ ", - "Accounts_ForgetUserSessionOnWindowClose": "윈ë„루를 ë‹«ì„ ë•Œì— ì‚¬ìš©ìž ì„¤ì •ì„ ì‚ì œ 합니다.", + "Accounts_ForgetUserSessionOnWindowClose": "ì°½ì„ ë‹«ì„ë•Œ ì‚¬ìš©ìž ì„¸ì…˜ì„ ì‚ì œí•©ë‹ˆë‹¤", "Accounts_Iframe_api_method": "API 메소드", "Accounts_Iframe_api_url": "API URL", "Accounts_iframe_enabled": "사용", @@ -61,8 +61,11 @@ "Accounts_OAuth_Custom_Token_Path": "Token 경로", "Accounts_OAuth_Custom_Token_Sent_Via": "í† í° ë³´ë‚¸ 비아", "Accounts_OAuth_Custom_Username_Field": "ì‚¬ìš©ìž ì´ë¦„ í•„ë“œ", - "Accounts_OAuth_Drupal": "Drupal Login ì´ í™œì„±í™” ë¨", - "Accounts_OAuth_Facebook": "Facebook 로그ì¸", + "Accounts_OAuth_Drupal": "듀팔 ë¡œê·¸ì¸ ì´ í™œì„±í™” ë˜ì—ˆìŠµë‹ˆë‹¤.", + "Accounts_OAuth_Drupal_callback_url": "듀팔 oAuth2 리다ì´ë ‰íŠ¸ URI", + "Accounts_OAuth_Drupal_id": "듀팔 oAuth2 í´ë¼ì´ì–¸íŠ¸ ID", + "Accounts_OAuth_Drupal_secret": "듀팔 oAuth2 í´ë¼ì´ì–¸íŠ¸ 비밀번호", + "Accounts_OAuth_Facebook": "페ì´ìŠ¤ë¶ 로그ì¸", "Accounts_OAuth_Facebook_callback_url": "페ì´ìŠ¤ ë¶ ì½œë°± URL", "Accounts_OAuth_Facebook_id": "Facebook 앱 ID", "Accounts_OAuth_Facebook_secret": "Facebook 암호", @@ -101,6 +104,7 @@ "Accounts_PasswordReset": "암호 ìž¬ì„¤ì •", "Accounts_OAuth_Proxy_host": "프ë¡ì‹œ 서버", "Accounts_OAuth_Proxy_services": "프ë¡ì‹œ 서비스", + "Accounts_Registration_AuthenticationServices_Default_Roles": "ì¸ì¦ì„œë¹„스용 기본 ì—í• ", "Accounts_Registration_AuthenticationServices_Enabled": "ì¸ì¦ ì„œë¹„ìŠ¤ì— ë“±ë¡", "Accounts_RegistrationForm": "ë“±ë¡ ì–‘ì‹", "Accounts_RegistrationForm_Disabled": "비활성화", @@ -111,43 +115,43 @@ "Accounts_RegistrationForm_SecretURL_Description": "ë‹¹ì‹ ì€ ë‹¹ì‹ ì˜ ë“±ë¡ URLì— ì¶”ê°€ë©ë‹ˆë‹¤ ìž„ì˜ì˜ 문ìžì—´ì„ ì œê³µí•´ì•¼í•©ë‹ˆë‹¤. 예 : https://demo.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp": "íšŒì› ê°€ìž…ì€ ì´ë¦„ í•„ìš”", "Accounts_SetDefaultAvatar": "기본 아바타 ì„¤ì •", - "Accounts_ShowFormLogin": "보기 ì–‘ì‹ ê¸°ë°˜ 로그ì¸", - "Accounts_UseDefaultBlockedDomainsList": "기본값 사용 차단 ëœ ë„ë©”ì¸ ëª©ë¡", - "Accounts_UseDNSDomainCheck": "DNS ë„ë©”ì¸ í™•ì¸ì„ 사용하여", + "Accounts_ShowFormLogin": "í¼ë°©ì‹ ë¡œê·¸ì¸ ë³´ê¸°", + "Accounts_UseDefaultBlockedDomainsList": "기본 차단 ë„ë©”ì¸ë¦¬ìŠ¤íŠ¸ 사용", + "Accounts_UseDNSDomainCheck": "DNS ë„ë©”ì¸ í™•ì¸ ì‚¬ìš©", "Accounts_UserAddedEmail_Default": "<h2> ì— ì˜¤ì‹ ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ <h1> [Site_Name] </h1></h2><p> [Site_URL]ë¡œ ì´ë™í•˜ì—¬ ì˜¤ëŠ˜ë‚ ìµœê³ ì˜ ì˜¤í”ˆ 소스 채팅 솔루션ì„ë³´ì‹ì‹œì˜¤! </p><p> [email]ê³¼ 비밀번호 : [password] ë‹¹ì‹ ì€ ë‹¹ì‹ ì˜ ì´ë©”ì¼ì„ 사용하여 ë¡œê·¸ì¸ í• ìˆ˜ 있습니다. ë‹¹ì‹ ì€ ì²˜ìŒ ë¡œê·¸ì¸ í›„ 변경해야 í• ìˆ˜ 있습니다.", "Accounts_UserAddedEmail_Description": "다ìŒê³¼ ê°™ì€ ìžë¦¬ë¥¼ ì‚¬ìš©í• ìˆ˜ 있습니다 : <br /><ul><li> ê°ê° 사용ìžì˜ ì „ì²´ ì´ë¦„, ì´ë¦„ ë˜ëŠ” 성ì„위한 [name], [fname], [lname]. </li><li> 사용ìžì˜ ì´ë©”ì¼ [email]. </li><li> 사용ìžì˜ 비밀번호 [password]. </li><li> [Site_Name]와 [Site_URL] ê°ê° ì‘ìš© 프로그램 ì´ë¦„ ë° URL합니다. </li></ul>", - "Accounts_UserAddedEmailSubject_Default": "ë‹¹ì‹ ì´ ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤ [Site_Name]", + "Accounts_UserAddedEmailSubject_Default": "ë‹¹ì‹ ì´ì€ [Site_Name] ì— ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤", "Activate": "활성화", "Activity": "활ë™", "Add": "추가", - "Add_agent": "ì—ì´ì „트를 추가", + "Add_agent": "ìƒë‹´ì‚¬ 추가", "Add_custom_oauth": "ì‚¬ìš©ìž ì •ì˜ OAuth 추가", "Add_Domain": "ë„ë©”ì¸ ì¶”ê°€", "Add_manager": "ê´€ë¦¬ìž ì¶”ê°€", "Add_user": "ì‚¬ìš©ìž ì¶”ê°€", "Add_User": "ì‚¬ìš©ìž ì¶”ê°€", "Add_users": "ì‚¬ìš©ìž ì¶”ê°€", - "Adding_OAuth_Services": "OAuth는 서비스 추가", - "Adding_permission": "ê¶Œí•œì„ ì¶”ê°€", - "Adding_user": "추가 사용ìž", - "Additional_emails": "추가 E - ë©”ì¼", + "Adding_OAuth_Services": "OAuth 서비스 추가", + "Adding_permission": "권한 추가", + "Adding_user": "ì‚¬ìš©ìž ì¶”ê°€", + "Additional_emails": "추가 ì´ë©”ì¼", "Additional_Feedback": "추가 ì˜ê²¬", "Administration": "관리", - "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "OAuth2를 ì¸ì¦ í•œ 후, 사용ìžëŠ”ì´ URLë¡œ ë¦¬ë””ë ‰ì…˜ë©ë‹ˆë‹¤", - "Agent": "ì—ì´ì „트", - "Agent_added": "ì—ì´ì „트는 추가", - "Agent_removed": "ì—ì´ì „트 ì œê±°", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Oauth2 ì¸ì¦í›„ 사용ìžëŠ” ì´ URLë¡œ ì´ë™ë©ë‹ˆë‹¤", + "Agent": "ìƒë‹´ì‚¬", + "Agent_added": "ìƒë‹´ì‚¬ê°€ 추가ë˜ì—ˆìŠµë‹ˆë‹¤", + "Agent_removed": "ìƒë‹´ì‚¬ê°€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤", "Alias": "별명", - "Alias_Format": "ë³„ì¹ í˜•ì‹", + "Alias_Format": "별명 형ì‹", "Alias_Set": "ë³„ì¹ ì„¤ì •", "All": "ëª¨ë“ ", "All_channels": "ëª¨ë“ ì±„ë„", "All_logs": "ëª¨ë“ ë¡œê·¸", "All_messages": "ëª¨ë“ ë©”ì‹œì§€", - "Allow_Invalid_SelfSigned_Certs": "ìž˜ëª»ëœ Self-Signed Certs 허용", - "Allow_Invalid_SelfSigned_Certs_Description": "ë§í¬ í™•ì¸ ë° ë¯¸ë¦¬ë³´ê¸° 무효 ë° ìžì²´ 서명 ëœ SSL ì¸ì¦ì„œì˜ 허용.", - "Allow_switching_departments": "방문ìžê°€ 부서를 ë³€ê²½í• ìˆ˜ 있ë„ë¡ í—ˆìš©í•¨", - "Analytics_features_enabled": "기능 활성화", + "Allow_Invalid_SelfSigned_Certs": "ìž˜ëª»ëœ ìžì²´ì„œëª… Certs 를 허용합니다", + "Allow_Invalid_SelfSigned_Certs_Description": "ë§í¬í™•ì¸ ê³¼ í”„ë¦¬ë·°ì— ìž˜ëª»ëœ ìžì²´ì„œëª… Certs 를 허용합니다.", + "Allow_switching_departments": "방문ìžê°€ 부서를 ë³€ê²½í• ìˆ˜ 있ë„ë¡ í—ˆìš©í•©ë‹ˆë‹¤", + "Analytics_features_enabled": "ê¸°ëŠ¥ì´ í™œì„±í™” ë˜ì—ˆìŠµë‹ˆë‹¤", "Analytics_features_messages_Description": "사용ìžê°€ ë©”ì‹œì§€ì— ëŒ€í•´ 수행 í–‰ë™ê³¼ ê´€ë ¨ëœ ì‚¬ìš©ìž ì •ì˜ ì´ë²¤íŠ¸ë¥¼ 추ì 합니다.", "Analytics_features_rooms_Description": "ì±„ë„ ë˜ëŠ” 그룹 (ì‚ì œë‘ê³ ìž‘ì„±)ì— ëŒ€í•œ ìž‘ì—…ì— ê´€ë ¨ëœ ì‚¬ìš©ìž ì •ì˜ ì´ë²¤íŠ¸ë¥¼ 추ì 합니다.", "Analytics_features_users_Description": "ì‚¬ìš©ìž (암호 ìž¬ì„¤ì • 시간, 프로필 사진 변경 등)ì— ê´€ë ¨ ìž‘ì—…ì— ê´€ë ¨ëœ ì‚¬ìš©ìž ì •ì˜ ì´ë²¤íŠ¸ë¥¼ 추ì 합니다.", diff --git a/packages/rocketchat-i18n/i18n/no.i18n.json b/packages/rocketchat-i18n/i18n/no.i18n.json index d1f6bfb211eccddde2a17c9c3008d8043a9e8833..e71892775f9f6456fcbe31f9c0484a76d624eaa7 100644 --- a/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/packages/rocketchat-i18n/i18n/no.i18n.json @@ -1,3 +1,68 @@ { - "0_Errors_Only": "0 - Kun Feil" + "0_Errors_Only": "0 - Kun Feil", + "1_Errors_and_Information": "1 - Feil og informasjon", + "2_Erros_Information_and_Debug": "2 - Feil, Informasjon og Feilsøking", + "403": "Forbudt", + "500": "Intern server feil", + "Accept": "Aksepter", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aksepter innkommende livechat selv om det ikke er noen online", + "Accept_with_no_online_agents": "Aksepter uten online agenter", + "Access_not_authorized": "Aksepter uautoriserte", + "Accounts_Enrollment_Email_Subject_Default": "Velkommen til [Site_Name]", + "Accounts_PasswordReset": "Reset passord", + "Accounts_RegistrationForm_Disabled": "Deaktivert", + "Accounts_RegistrationForm_Public": "Offentlig", + "Activate": "Aktiver", + "Activity": "Aktivitet", + "Add": "Legg til", + "Add_agent": "Legg til agent", + "Add_Domain": "Legg til domene", + "Add_manager": "Legg til leder", + "Add_user": "Legg til bruker", + "Add_User": "Legg til bruker", + "Add_users": "Legg til brukere", + "Adding_permission": "Legger til rettigheter", + "Adding_user": "Legger til bruker", + "Additional_emails": "Ekstra e-postadresser", + "Additional_Feedback": "Ekstra tilbakemelding", + "Administration": "Administrasjon", + "Agent": "Agent", + "Agent_added": "Lagt til agent", + "Agent_removed": "Fjernet agent", + "All": "Alle", + "All_channels": "Alle kanaler", + "All_logs": "Aller logger", + "All_messages": "Alle meldinger", + "Archive": "Arkiv", + "are_also_typing": "skriver ogsÃ¥", + "are_typing": "skriver", + "Are_you_sure": "Er du sikker?", + "Author": "Forfatter", + "Available": "Tilgjengelig", + "Available_agents": "Tilgjengelige agenter", + "away": "borte", + "Away": "Borte", + "away_female": "borte", + "Away_female": "Borte", + "away_male": "borte", + "Away_male": "Borte", + "Back": "Tilbake", + "Back_to_applications": "Tilbake til programmer", + "Back_to_integrations": "Tilbake til integrasjoner", + "Back_to_login": "Tilbake til login", + "Back_to_permissions": "Tilbake til rettigheter", + "Block_User": "Blokker bruker", + "bold": "fet", + "busy": "opptatt", + "Busy": "Opptatt", + "busy_female": "opptatt", + "Busy_female": "Opptatt", + "busy_male": "opptatt", + "Busy_male": "Opptatt", + "by": "av", + "Content": "Innhold", + "Cancel": "Avbryt", + "Cancel_message_input": "Avbryt", + "channel": "kanal", + "Channel": "Kanal" } \ No newline at end of file diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index df3ec4bf828bd9b084fafb3d6b1b90224370880f..06763c0a97e6526263fb7e24728b3618bb9af608 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -65,15 +65,15 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { if (syncUserData && syncUserDataFieldMap) { const fieldMap = JSON.parse(syncUserDataFieldMap); const userData = {}; - const emailList = []; _.map(fieldMap, function(userField, ldapField) { - if (!ldapUser.object.hasOwnProperty(ldapField)) { - return; - } - switch (userField) { case 'email': + if (!ldapUser.object.hasOwnProperty(ldapField)) { + logger.debug(`user does not have attribute: ${ ldapField }`); + return; + } + if (_.isObject(ldapUser.object[ldapField])) { _.map(ldapUser.object[ldapField], function(item) { emailList.push({ address: item, verified: true }); @@ -84,8 +84,37 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { break; case 'name': - if (user.name !== ldapUser.object[ldapField]) { - userData.name = ldapUser.object[ldapField]; + const templateRegex = /#{(\w+)}/gi; + let match = templateRegex.exec(ldapField); + let tmpLdapField = ldapField; + + if (match == null) { + if (!ldapUser.object.hasOwnProperty(ldapField)) { + logger.debug(`user does not have attribute: ${ ldapField }`); + return; + } + tmpLdapField = ldapUser.object[ldapField]; + } else { + logger.debug('template found. replacing values'); + while (match != null) { + const tmplVar = match[0]; + const tmplAttrName = match[1]; + + if (!ldapUser.object.hasOwnProperty(tmplAttrName)) { + logger.debug(`user does not have attribute: ${ tmplAttrName }`); + return; + } + + const attrVal = ldapUser.object[tmplAttrName]; + logger.debug(`replacing template var: ${ tmplVar } with value from ldap: ${ attrVal }`); + tmpLdapField = tmpLdapField.replace(tmplVar, attrVal); + match = templateRegex.exec(ldapField); + } + } + + if (user.name !== tmpLdapField) { + userData.name = tmpLdapField; + logger.debug(`user.name changed to: ${ tmpLdapField }`); } break; } diff --git a/packages/rocketchat-lib/client/lib/roomTypes.js b/packages/rocketchat-lib/client/lib/roomTypes.js index 9cfc41585961c5713d53ed8b22ba0e8d99c184e9..fdc926c063da070bddc26e0ed9bd95dbcfb640b8 100644 --- a/packages/rocketchat-lib/client/lib/roomTypes.js +++ b/packages/rocketchat-lib/client/lib/roomTypes.js @@ -22,7 +22,7 @@ RocketChat.roomTypes = new class extends roomTypesCommon { return _.map(list, (t) => t.identifier); } getUserStatus(roomType, roomId) { - this.roomTypes[roomType] && typeof this.roomTypes[roomType].getUserStatus === 'function' && this.roomTypes[roomType].getUserStatus(roomId); + return this.roomTypes[roomType] && typeof this.roomTypes[roomType].getUserStatus === 'function' && this.roomTypes[roomType].getUserStatus(roomId); } findRoom(roomType, identifier, user) { return this.roomTypes[roomType] && this.roomTypes[roomType].findRoom(identifier, user); diff --git a/packages/rocketchat-lib/lib/settings.js b/packages/rocketchat-lib/lib/settings.js index 9c14943dac63ee4a0d30ae4b36efb2f98dd2d49d..ecdd2c87c4b3e663c5d6129c4f09352c38c2f161 100644 --- a/packages/rocketchat-lib/lib/settings.js +++ b/packages/rocketchat-lib/lib/settings.js @@ -63,9 +63,9 @@ RocketChat.settings = { return _(actions).reduceRight(_.wrap, (err, success) => callback(err, success))(); }, load(key, value, initialLoad) { - ['*', key].forEach(key => { - if (RocketChat.settings.callbacks[key]) { - RocketChat.settings.callbacks[key].forEach(callback => callback(key, value, initialLoad)); + ['*', key].forEach(item => { + if (RocketChat.settings.callbacks[item]) { + RocketChat.settings.callbacks[item].forEach(callback => callback(key, value, initialLoad)); } }); Object.keys(RocketChat.settings.regexCallbacks).forEach(cbKey => { diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info index ce43333a4eb3d13b5c1194a345a71480e9311583..fbad42938bafe8b016313731862d336e9f2bad8d 100644 --- a/packages/rocketchat-lib/rocketchat.info +++ b/packages/rocketchat-lib/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "0.56.0-develop" + "version": "0.57.0-develop" } diff --git a/packages/rocketchat-lib/server/lib/PushNotification.js b/packages/rocketchat-lib/server/lib/PushNotification.js index fd1bdb8db6047f74c112d76ed6a8da96a97eddba..fca87e6455a869ddca2569533480a4802b12dec1 100644 --- a/packages/rocketchat-lib/server/lib/PushNotification.js +++ b/packages/rocketchat-lib/server/lib/PushNotification.js @@ -16,7 +16,7 @@ class PushNotification { return hash; } - send({ roomName, roomId, username, message, usersTo, payload }) { + send({ roomName, roomId, username, message, usersTo, payload, badge = 1 }) { let title; if (roomName && roomName !== '') { title = `${ roomName }`; @@ -27,7 +27,7 @@ class PushNotification { const icon = RocketChat.settings.get('Assets_favicon_192').url || RocketChat.settings.get('Assets_favicon_192').defaultUrl; const config = { from: 'push', - badge: 1, + badge, sound: 'default', title, text: message, diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js index ea6ef470118a7f2b16568127f7e02eb2154c4700..b4802e357f85740c9ba1cef4042f87e89cd899d7 100644 --- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js @@ -1,6 +1,14 @@ /* globals Push */ import moment from 'moment'; +function getBadgeCount(userId) { + const subscriptions = RocketChat.models.Subscriptions.findUnreadByUserId(userId).fetch(); + + return subscriptions.reduce((unread, sub) => { + return sub.unread + unread; + }, 0); +} + RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // skips this callback if the message was edited if (message.editedAt) { @@ -152,6 +160,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { roomId: message.rid, username: push_username, message: push_message, + badge: getBadgeCount(userOfMention._id), payload: { host: Meteor.absoluteUrl(), rid: message.rid, @@ -299,23 +308,25 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { if (userIdsToPushNotify.length > 0) { if (Push.enabled === true) { - RocketChat.PushNotification.send({ - roomId: message.rid, - roomName: push_room, - username: push_username, - message: push_message, - payload: { - host: Meteor.absoluteUrl(), - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - }, - usersTo: { - userId: { - $in: userIdsToPushNotify + // send a push notification for each user individually (to get his/her badge count) + userIdsToPushNotify.forEach((userIdToNotify) => { + RocketChat.PushNotification.send({ + roomId: message.rid, + roomName: push_room, + username: push_username, + message: push_message, + badge: getBadgeCount(userIdToNotify), + payload: { + host: Meteor.absoluteUrl(), + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + }, + usersTo: { + userId: userIdToNotify } - } + }); }); } } diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee index 95f12bff51ff2b341bc873211297db331cc5f31e..2eb4f13d484a3bf40ba3c8381b718ae7fc644ee6 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.coffee +++ b/packages/rocketchat-lib/server/models/Subscriptions.coffee @@ -126,6 +126,14 @@ class ModelSubscriptions extends RocketChat.models._Base return @find query + findUnreadByUserId: (userId) -> + query = + 'u._id': userId + unread: + $gt: 0 + + return @find query, fields: unread: 1 + # UPDATE archiveByRoomId: (roomId) -> query = diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index f222176bb388af3e33fd09fd9ab7809bd5e3622a..bd77f407b7d70c0e630c151e1142befee161be11 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -481,6 +481,20 @@ RocketChat.settings.addGroup('Email', function() { }); }); this.section('SMTP', function() { + this.add('SMTP_Protocol', 'smtp', { + type: 'select', + values: [ + { + key: 'smtp', + i18nLabel: 'smtp' + }, { + key: 'smtps', + i18nLabel: 'smtps' + } + ], + env: true, + i18nLabel: 'Protocol' + }); this.add('SMTP_Host', '', { type: 'string', env: true, @@ -491,6 +505,11 @@ RocketChat.settings.addGroup('Email', function() { env: true, i18nLabel: 'Port' }); + this.add('SMTP_Pool', true, { + type: 'boolean', + env: true, + i18nLabel: 'Pool' + }); this.add('SMTP_Username', '', { type: 'string', env: true, diff --git a/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js b/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js index 5a2fb4d672958b0254562900b5e0e3e76ad5288d..8a3601061aa628763199130f1c8761aea2a0def4 100644 --- a/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js +++ b/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js @@ -1,14 +1,22 @@ const buildMailURL = _.debounce(function() { console.log('Updating process.env.MAIL_URL'); + if (RocketChat.settings.get('SMTP_Host')) { - process.env.MAIL_URL = 'smtp://'; + process.env.MAIL_URL = `${ RocketChat.settings.get('SMTP_Protocol') }://`; + if (RocketChat.settings.get('SMTP_Username') && RocketChat.settings.get('SMTP_Password')) { process.env.MAIL_URL += `${ encodeURIComponent(RocketChat.settings.get('SMTP_Username')) }:${ encodeURIComponent(RocketChat.settings.get('SMTP_Password')) }@`; } + process.env.MAIL_URL += encodeURIComponent(RocketChat.settings.get('SMTP_Host')); + if (RocketChat.settings.get('SMTP_Port')) { - return process.env.MAIL_URL += `:${ parseInt(RocketChat.settings.get('SMTP_Port')) }`; + process.env.MAIL_URL += `:${ parseInt(RocketChat.settings.get('SMTP_Port')) }`; } + + process.env.MAIL_URL += `?pool=${ RocketChat.settings.get('SMTP_Pool') }`; + + return process.env.MAIL_URL; } }, 500); @@ -34,6 +42,14 @@ RocketChat.settings.onload('SMTP_Password', function(key, value) { } }); +RocketChat.settings.onload('SMTP_Protocol', function() { + return buildMailURL(); +}); + +RocketChat.settings.onload('SMTP_Pool', function() { + return buildMailURL(); +}); + Meteor.startup(function() { return buildMailURL(); }); diff --git a/packages/rocketchat-livechat/app/i18n/de.i18n.json b/packages/rocketchat-livechat/app/i18n/de.i18n.json index 2474b11a94af432dd4ce804d57cccf4c25c02241..625393ec2916c035c7c79ea01c4d942700d3c4d3 100644 --- a/packages/rocketchat-livechat/app/i18n/de.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/de.i18n.json @@ -2,9 +2,11 @@ "Additional_Feedback": "Zusätzliches Feedback", "Appearance": "Erscheinungsbild", "Are_you_sure_do_you_want_end_this_chat": "Sind Sie sich sicher diesen Chat zu beenden?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Sind Sie sich sicher diesen Chat zu beenden und den Bereich zu wechseln?", "Cancel": "Abbrechen", "Change": "Ändern", "Chat_ended": "Chat beendet!", + "Choose_a_new_department": "Wähle einen neuen Bereich", "Close_menu": "Menü schließen", "Conversation_finished": "Gespräch beendet", "End_chat": "Chat beenden", @@ -17,6 +19,7 @@ "No": "Nein", "Options": "Optionen", "Please_answer_survey": "Bitte nehmen Sie sich einen Moment Zeit, um kurz einige Fragen zu dem Gespräch zu beantworten.", + "Please_choose_a_department": "Wähle einen Bereich", "Please_fill_name_and_email": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.", "Powered_by": "Unterstützt von", "Request_video_chat": "Video-Chat anfragen", diff --git a/packages/rocketchat-livechat/app/i18n/fr.i18n.json b/packages/rocketchat-livechat/app/i18n/fr.i18n.json index 61c3e8275f9535f5f04d4fff47883223ea071d66..1e07751cfee850f71f29c8db077827c480b1a6f9 100644 --- a/packages/rocketchat-livechat/app/i18n/fr.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/fr.i18n.json @@ -2,30 +2,30 @@ "Additional_Feedback": "Commentaires supplémentaires", "Appearance": "Apparence", "Are_you_sure_do_you_want_end_this_chat": "Êtes-vous sûr de vouloir mettre fin à cette conversation ?", - "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Etes vous sûr de vouloir terminer ce chat en direct et changer de département ?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Etes vous sûr de vouloir terminer ce chat en direct et changer de service ?", "Cancel": "Annuler", "Change": "Changer", "Chat_ended": "Conversation terminée !", - "Choose_a_new_department": "Choisir un nouveau département", + "Choose_a_new_department": "Choisir un nouveau service", "Close_menu": "Fermer le menu", "Conversation_finished": "Conversation terminée", "End_chat": "Mettre fin à la conversation", - "How_friendly_was_the_chat_agent": "L'assistant du chat était-il sympathique ?", - "How_knowledgeable_was_the_chat_agent": "Les réponses de l'assistant du chat était-elles adaptées ?", - "How_responsive_was_the_chat_agent": "L'assistant du chat a-t-il répondu à vos questions ?", + "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il sympathique ?", + "How_knowledgeable_was_the_chat_agent": "Les réponses de votre interlocuteur étaient-elles adaptées ?", + "How_responsive_was_the_chat_agent": "Votre interlocuteur a-t-il répondu à vos questions ?", "How_satisfied_were_you_with_this_chat": "Êtes-vous satisfait de ce chat?", "Installation": "Installation", "New_messages": "Nouveaux messages", "No": "Non", "Options": "Options", "Please_answer_survey": "Merci de prendre un moment pour répondre à un sondage rapide à propos de ce chat ", - "Please_choose_a_department": "Merci de choisir un département", - "Please_fill_name_and_email": "Veuillez remplir votre nom et votre adresse e-mail", + "Please_choose_a_department": "Merci de choisir un service", + "Please_fill_name_and_email": "Veuillez saisir votre nom et votre adresse e-mail", "Powered_by": "Propulsé par", "Request_video_chat": "Demander un chat vidéo", - "Select_a_department": "Sélectionner un département", - "Switch_department": "Changer de département", - "Department_switched": "Département changé", + "Select_a_department": "Sélectionner un service", + "Switch_department": "Changer de service", + "Department_switched": "Changement de service effectué", "Send": "Envoyer", "Skip": "Passer", "Start_Chat": "Démarrer un chat", diff --git a/packages/rocketchat-livechat/app/i18n/ko.i18n.json b/packages/rocketchat-livechat/app/i18n/ko.i18n.json index 569097d1f0411eafdfb62e43a354fc82712b98e8..04b448a9da550beb7da684f58e86a97f6c95f8ba 100644 --- a/packages/rocketchat-livechat/app/i18n/ko.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/ko.i18n.json @@ -2,30 +2,30 @@ "Additional_Feedback": "추가 ì˜ê²¬", "Appearance": "모양", "Are_you_sure_do_you_want_end_this_chat": "ì´ ì±„íŒ…ì„ ì •ë§ ëë‚´ì‹œê² ìŠµë‹ˆê¹Œ?", - "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "ì •ë§ ì´ ì±„íŒ…ì„ ì¢…ë£Œí•˜ê³ ë¶€ì„œë¥¼ ë³€ê²½í•˜ê² ìŠµë‹ˆê¹Œ?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "현재 ì§„í–‰ì¤‘ì¸ ì±„íŒ…ì„ ì¢…ë£Œí•˜ê³ ë¶€ì„œë¥¼ ë³€ê²½í•˜ì‹œê² ìŠµë‹ˆê¹Œ?", "Cancel": "취소", "Change": "변경", - "Chat_ended": "채팅 종료", - "Choose_a_new_department": "새로운 부서를 ì„ íƒí•˜ì„¸ìš”.", + "Chat_ended": "ì±„íŒ…ì´ ì¢…ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. ", + "Choose_a_new_department": "새 부서를 ì„ íƒí•´ì£¼ì„¸ìš”", "Close_menu": "메뉴 닫기", "Conversation_finished": "대화 종료ë¨", "End_chat": "채팅 ë남", - "How_friendly_was_the_chat_agent": "채팅 담당ìžëŠ” 얼마나 ì¹œì ˆí–ˆë‚˜ìš”?", - "How_knowledgeable_was_the_chat_agent": "채팅 ì—ì´ì „íŠ¸ì˜ ê¸°ë°˜ 지ì‹ì´ í’부했나요?", - "How_responsive_was_the_chat_agent": "채팅 담당ìžëŠ” 얼마나 ë¹ ë¥´ê²Œ ì‘답했나요?", - "How_satisfied_were_you_with_this_chat": "ì±„íŒ…ì— ì–¼ë§ˆë‚˜ 만족했나요?", + "How_friendly_was_the_chat_agent": "ìƒë‹´ì‚¬ê°€ ì¹œì ˆí–ˆë‚˜ìš”?", + "How_knowledgeable_was_the_chat_agent": "ìƒë‹´ì‚¬ì˜ ê´€ë ¨ 업무 지ì‹ì´ 충분했나요?", + "How_responsive_was_the_chat_agent": "삼담사가 ë¹ ë¥´ê²Œ ì‘답했나요?", + "How_satisfied_were_you_with_this_chat": "채팅 ë‚´ìš©ì— ì–¼ë§ˆë‚˜ 만족 하였나요?", "Installation": "설치", "New_messages": "새 메시지", "No": "아니오", "Options": "옵션", "Please_answer_survey": "ì´ ì±„íŒ…ì— ëŒ€í•œ 간단한 설문 ì¡°ì‚¬ì— ì‘답하기 위해 ìž ì‹œ 시간ì„ë‚´ì–´ 주시기 ë°”ëžë‹ˆë‹¤", - "Please_choose_a_department": "부서를 ì„ íƒí•˜ì„¸ìš”.", + "Please_choose_a_department": "부서를 ì„ íƒí•´ì£¼ì„¸ìš”", "Please_fill_name_and_email": "ì´ë¦„ê³¼ ì´ë©”ì¼ì„ ìž…ë ¥í•˜ì„¸ìš”", - "Powered_by": "ì— ì˜í•´ 구ë™", + "Powered_by": "지ì›ì„받는", "Request_video_chat": "비디오채팅 ìš”ì²", "Select_a_department": "부서를 ì„ íƒí•´ì£¼ì„¸ìš”", "Switch_department": "부서 변경", - "Department_switched": "부서가 변경ë˜ì—ˆìŠµë‹ˆë‹¤.", + "Department_switched": "부서가 변경ë˜ì—ˆìŠµë‹ˆë‹¤", "Send": "ì „ì†¡", "Skip": "건너뛰기", "Start_Chat": "채팅 시작", @@ -33,13 +33,14 @@ "Survey_instructions": "ë‹¹ì‹ ì˜ ë§Œì¡±ë„를 í‰ê°€í•´ 주세요. ë§¤ìš°ë¶ˆë§Œì€ 1, 매우 ë§Œì¡±ì€ 5 입니다.", "Thank_you_for_your_feedback": "ì˜ê²¬ì„ ë³´ë‚´ 주셔서 ê°ì‚¬í•©ë‹ˆë‹¤", "Thanks_We_ll_get_back_to_you_soon": "ê°ì‚¬í•©ë‹ˆë‹¤! 곧 다시 ì—°ë½ë“œë¦¬ê² 습니다", - "transcript_sent": "채팅 ë‚´ìš©ì„ ë°œì†¡í–ˆìŠµë‹ˆë‹¤.", - "Type_your_email": "ì´ë©”ì¼ì„ ìž…ë ¥", - "Type_your_message": "메시지를 ìž…ë ¥", - "Type_your_name": "ë‹¹ì‹ ì˜ ì´ë¦„ì„ ìž…ë ¥", - "User_joined": "ì‚¬ìš©ìž ê°€ìž…", - "User_left": "ì‚¬ìš©ìž ì™¼ìª½", - "We_are_offline_Sorry_for_the_inconvenience": "우리는 오프ë¼ì¸ ìƒíƒœìž…니다. ë¶ˆíŽ¸ì„ ë“œë ¤ 죄송합니다.", + "transcript_sent": "채팅 ë‚´ìš©ì„ ë°œì†¡í–ˆìŠµë‹ˆë‹¤", + "Type_your_email": "ì´ë©”ì¼ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”", + "Type_your_message": "메시지를 ìž…ë ¥í•´ì£¼ì„¸ìš”", + "Type_your_name": "ì´ë¦„ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”", + "User_joined": "사용ìžê°€ 참여하였습니다", + "User_left": "사용ìžê°€ ë– ë‚¬ìŠµë‹ˆë‹¤", + "We_are_offline_Sorry_for_the_inconvenience": "현재 오프ë¼ì¸ ìƒíƒœìž…니다. ë¶ˆíŽ¸ì„ ë“œë ¤ 죄송합니다", "Yes": "예", + "You": "ë‹¹ì‹ ", "You_must_complete_all_fields": "ëª¨ë“ í•„ë“œë¥¼ 작성해야합니다" } \ No newline at end of file diff --git a/packages/rocketchat-livechat/app/i18n/no.i18n.json b/packages/rocketchat-livechat/app/i18n/no.i18n.json index d6955adb8572b4c553abd56118203f4cf81085bd..539088fa6dc64634a059a906eb8aa1673a967c2a 100644 --- a/packages/rocketchat-livechat/app/i18n/no.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/no.i18n.json @@ -1,4 +1,29 @@ { "Additional_Feedback": "Tilleggs Tilbakemelding", - "No": "Nei" + "Appearance": "Utseende", + "Are_you_sure_do_you_want_end_this_chat": "Er du sikker pÃ¥ at du vil avslutte denne samtalen?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Er du sikker pÃ¥ at du vil avslutte denne samtalen og bytte avdeling?", + "Cancel": "Avbryt", + "Change": "Endre", + "Chat_ended": "Samtalen er avsluttet!", + "Choose_a_new_department": "Velg ny avdeling", + "Close_menu": "Lukk meny", + "Conversation_finished": "Samtalen er avsluttet", + "End_chat": "Avslutt samtale", + "How_friendly_was_the_chat_agent": "Hvor vennlig var personen du pratet med?", + "How_responsive_was_the_chat_agent": "Hvor raskt svarte personen du pratet med?", + "How_satisfied_were_you_with_this_chat": "Er du fornøyd med samtalen?", + "Installation": "Installasjon", + "New_messages": "Ny melding", + "No": "Nei", + "Options": "Egenskaper", + "Send": "Send", + "Skip": "Hopp over", + "Start_Chat": "Start samtale", + "Survey": "Undersøkelse", + "Type_your_message": "Skriv inn din beskjed", + "Type_your_name": "Skriv inn ditt navn", + "Yes": "Ja", + "You": "Deg", + "You_must_complete_all_fields": "Du mÃ¥ fylle inn alle feltene" } \ No newline at end of file diff --git a/packages/rocketchat-slashcommands-mute/server/mute.js b/packages/rocketchat-slashcommands-mute/server/mute.js index 38273ff18feeea43344ccd36e2e675efd4caeeef..21601cebe0807cc5ea9a2d02f4cb79240fb37f17 100644 --- a/packages/rocketchat-slashcommands-mute/server/mute.js +++ b/packages/rocketchat-slashcommands-mute/server/mute.js @@ -26,7 +26,7 @@ RocketChat.slashCommands.add('mute', function Mute(command, params, item) { }); return; } - if ((room.usernames || []).includes(username)) { + if ((room.usernames || []).includes(username) === false) { RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { _id: Random.id(), rid: item.rid, diff --git a/packages/rocketchat-slashcommands-mute/server/unmute.js b/packages/rocketchat-slashcommands-mute/server/unmute.js index c5a3f02cfa108f79dff266141c707cf0c961a748..0d5d6a93e12e94494702711645a7143215570c2f 100644 --- a/packages/rocketchat-slashcommands-mute/server/unmute.js +++ b/packages/rocketchat-slashcommands-mute/server/unmute.js @@ -26,7 +26,7 @@ RocketChat.slashCommands.add('unmute', function Unmute(command, params, item) { }, user.language) }); } - if ((room.usernames || []).includes(username)) { + if ((room.usernames || []).includes(username) === false) { return RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { _id: Random.id(), rid: item.rid, diff --git a/packages/rocketchat-theme/server/server.js b/packages/rocketchat-theme/server/server.js index 940e4a1fdddfd7f53919abb4298c0725976360ee..16b254185f6a216f9efd063cebab78132f276b46 100644 --- a/packages/rocketchat-theme/server/server.js +++ b/packages/rocketchat-theme/server/server.js @@ -71,17 +71,16 @@ RocketChat.theme = new class { this.compileDelayed = _.debounce(Meteor.bindEnvironment(this.compile.bind(this)), 100); Meteor.startup(() => { RocketChat.settings.onAfterInitialLoad(() => { - RocketChat.settings.get('*', Meteor.bindEnvironment((key, value) => { + RocketChat.settings.get(/^theme-./, Meteor.bindEnvironment((key, value) => { if (key === 'theme-custom-css' && value != null) { this.customCSS = value; - } else if (/^theme-.+/.test(key) === true) { + } else { const name = key.replace(/^theme-[a-z]+-/, ''); if (this.variables[name] != null) { this.variables[name].value = value; } - } else { - return; } + this.compileDelayed(); })); }); @@ -89,7 +88,6 @@ RocketChat.theme = new class { } compile() { - let content = [this.getVariablesAsLess()]; content.push(...this.files.map((name) => Assets.getText(name))); diff --git a/packages/rocketchat-ui-account/client/account.coffee b/packages/rocketchat-ui-account/client/account.coffee deleted file mode 100644 index 21a9874c2416e37cf31dd1cf312ca400eca0ee80..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/account.coffee +++ /dev/null @@ -1,4 +0,0 @@ -Template.account.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "accountFlex" - SideNav.openFlex() diff --git a/packages/rocketchat-ui-account/client/account.js b/packages/rocketchat-ui-account/client/account.js new file mode 100644 index 0000000000000000000000000000000000000000..214d55abe8c614d84b784a996c3180ca5219db58 --- /dev/null +++ b/packages/rocketchat-ui-account/client/account.js @@ -0,0 +1,6 @@ +Template.account.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); diff --git a/packages/rocketchat-ui-account/client/accountFlex.coffee b/packages/rocketchat-ui-account/client/accountFlex.coffee deleted file mode 100644 index 1e04bc7ce919da8250f2979a7f5823abe066cf95..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/accountFlex.coffee +++ /dev/null @@ -1,21 +0,0 @@ -Template.accountFlex.events - 'mouseenter header': -> - SideNav.overArrow() - - 'mouseleave header': -> - SideNav.leaveArrow() - - 'click header': -> - SideNav.closeFlex() - - 'click .cancel-settings': -> - SideNav.closeFlex() - - 'click .account-link': -> - menu.close() - -Template.accountFlex.helpers - allowUserProfileChange: -> - return RocketChat.settings.get("Accounts_AllowUserProfileChange") - allowUserAvatarChange: -> - return RocketChat.settings.get("Accounts_AllowUserAvatarChange") \ No newline at end of file diff --git a/packages/rocketchat-ui-account/client/accountFlex.js b/packages/rocketchat-ui-account/client/accountFlex.js new file mode 100644 index 0000000000000000000000000000000000000000..7d6d789084c824f1744a4c41b8c4509d732c4ef6 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountFlex.js @@ -0,0 +1,27 @@ +/*globals menu */ +Template.accountFlex.events({ + 'mouseenter header'() { + SideNav.overArrow(); + }, + 'mouseleave header'() { + SideNav.leaveArrow(); + }, + 'click header'() { + SideNav.closeFlex(); + }, + 'click .cancel-settings'() { + SideNav.closeFlex(); + }, + 'click .account-link'() { + menu.close(); + } +}); + +Template.accountFlex.helpers({ + allowUserProfileChange() { + return RocketChat.settings.get('Accounts_AllowUserProfileChange'); + }, + allowUserAvatarChange() { + return RocketChat.settings.get('Accounts_AllowUserAvatarChange'); + } +}); diff --git a/packages/rocketchat-ui-account/client/accountPreferences.coffee b/packages/rocketchat-ui-account/client/accountPreferences.coffee deleted file mode 100644 index 5ed2003c658c3cbd1259a8361af956d6b877accc..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/accountPreferences.coffee +++ /dev/null @@ -1,145 +0,0 @@ -import toastr from 'toastr' -Template.accountPreferences.helpers - audioAssets: -> - return RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList() || []; - - newMessageNotification: -> - return Meteor.user()?.settings?.preferences?.newMessageNotification || 'chime' - - newRoomNotification: -> - return Meteor.user()?.settings?.preferences?.newRoomNotification || 'door' - - languages: -> - languages = TAPi18n.getLanguages() - result = [] - for key, language of languages - result.push _.extend(language, { key: key }) - return _.sortBy(result, 'key') - - userLanguage: (key) -> - return (Meteor.user().language or defaultUserLanguage())?.split('-').shift().toLowerCase() is key - - checked: (property, value, defaultValue) -> - if not Meteor.user()?.settings?.preferences?[property]? and defaultValue is true - currentValue = value - else if Meteor.user()?.settings?.preferences?[property]? - currentValue = !!Meteor.user()?.settings?.preferences?[property] - - return currentValue is value - - selected: (property, value, defaultValue) -> - if not Meteor.user()?.settings?.preferences?[property] - return defaultValue is true - else - return Meteor.user()?.settings?.preferences?[property] == value - - highlights: -> - return Meteor.user()?.settings?.preferences?['highlights']?.join(', ') - - desktopNotificationEnabled: -> - return (KonchatNotification.notificationStatus.get() is 'granted') or (window.Notification && Notification.permission is "granted") - - desktopNotificationDisabled: -> - return (KonchatNotification.notificationStatus.get() is 'denied') or (window.Notification && Notification.permission is "denied") - - desktopNotificationDuration: -> - return Meteor.user()?.settings?.preferences?.desktopNotificationDuration - 0 - - showRoles: -> - return RocketChat.settings.get('UI_DisplayRoles'); - -Template.accountPreferences.onCreated -> - settingsTemplate = this.parentTemplate(3) - settingsTemplate.child ?= [] - settingsTemplate.child.push this - - @useEmojis = new ReactiveVar not Meteor.user()?.settings?.preferences?.useEmojis? or Meteor.user().settings.preferences.useEmojis - instance = @ - @autorun -> - if instance.useEmojis.get() - Tracker.afterFlush -> - $('#convertAsciiEmoji').show() - else - Tracker.afterFlush -> - $('#convertAsciiEmoji').hide() - - @clearForm = -> - @find('#language').value = localStorage.getItem('userLanguage') - - @save = -> - instance = @ - data = {} - - reload = false - selectedLanguage = $('#language').val() - - if localStorage.getItem('userLanguage') isnt selectedLanguage - localStorage.setItem 'userLanguage', selectedLanguage - data.language = selectedLanguage - reload = true - - data.newRoomNotification = $('select[name=newRoomNotification]').val() - data.newMessageNotification = $('select[name=newMessageNotification]').val() - data.useEmojis = $('input[name=useEmojis]:checked').val() - data.convertAsciiEmoji = $('input[name=convertAsciiEmoji]:checked').val() - data.saveMobileBandwidth = $('input[name=saveMobileBandwidth]:checked').val() - data.collapseMediaByDefault = $('input[name=collapseMediaByDefault]:checked').val() - data.viewMode = parseInt($('#viewMode').find('select').val()) - data.hideUsernames = $('#hideUsernames').find('input:checked').val() - data.hideRoles = $('#hideRoles').find('input:checked').val() - data.hideFlexTab = $('#hideFlexTab').find('input:checked').val() - data.hideAvatars = $('#hideAvatars').find('input:checked').val() - data.mergeChannels = $('#mergeChannels').find('input:checked').val() - data.sendOnEnter = $('#sendOnEnter').find('select').val() - data.unreadRoomsMode = $('input[name=unreadRoomsMode]:checked').val() - data.autoImageLoad = $('input[name=autoImageLoad]:checked').val() - data.emailNotificationMode = $('select[name=emailNotificationMode]').val() - data.highlights = _.compact(_.map($('[name=highlights]').val().split(','), (e) -> return _.trim(e))) - data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val() - data.unreadAlert = $('#unreadAlert').find('input:checked').val() - - Meteor.call 'saveUserPreferences', data, (error, results) -> - if results - toastr.success t('Preferences_saved') - instance.clearForm() - if reload - setTimeout -> - Meteor._reload.reload() - , 1000 - - if error - handleError(error) - -Template.accountPreferences.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "accountFlex" - SideNav.openFlex() - -Template.accountPreferences.events - 'click .submit button': (e, t) -> - t.save() - - 'change input[name=useEmojis]': (e, t) -> - t.useEmojis.set $(e.currentTarget).val() is '1' - - 'click .enable-notifications': -> - KonchatNotification.getDesktopPermission() - - 'click .test-notifications': -> - KonchatNotification.notify - duration: $('input[name=desktopNotificationDuration]').val() - payload: - sender: - username: 'rocket.cat' - title: TAPi18n.__('Desktop_Notification_Test') - text: TAPi18n.__('This_is_a_desktop_notification') - - 'change .audio': (e) -> - e.preventDefault() - audio = $(e.currentTarget).val() - if audio is 'none' - return - - if audio - $audio = $('audio#' + audio) - $audio?[0]?.play() diff --git a/packages/rocketchat-ui-account/client/accountPreferences.js b/packages/rocketchat-ui-account/client/accountPreferences.js new file mode 100644 index 0000000000000000000000000000000000000000..bfd096e5f0dfd909e26e1914d31786cc81a285b0 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountPreferences.js @@ -0,0 +1,180 @@ +/*globals defaultUserLanguage, KonchatNotification */ +import toastr from 'toastr'; +Template.accountPreferences.helpers({ + audioAssets() { + return (RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList()) || []; + }, + newMessageNotification() { + const user = Meteor.user(); + return (user && user.settings && user.settings.preferences && user.settings.preferences.newMessageNotification) || 'chime'; + }, + newRoomNotification() { + const user = Meteor.user(); + return (user && user.settings && user.settings.preferences && user.settings.preferences.newRoomNotification) || 'door'; + }, + languages() { + const languages = TAPi18n.getLanguages(); + + const result = Object.keys(languages).map((key) => { + const language = languages[key]; + return _.extend(language, { key }); + }); + + return _.sortBy(result, 'key'); + }, + userLanguage(key) { + const user = Meteor.user(); + let result = undefined; + if (user.language) { + result = user.language.split('-').shift().toLowerCase() === key; + } else if (defaultUserLanguage()) { + result = defaultUserLanguage().split('-').shift().toLowerCase() === key; + } + return result; + }, + checked(property, value, defaultValue) { + const user = Meteor.user(); + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); + let currentValue; + if (propertyeExists) { + currentValue = !!user.settings.preferences[property]; + } else if (!propertyeExists && defaultValue === true) { + currentValue = value; + } + return currentValue === value; + }, + selected(property, value, defaultValue) { + const user = Meteor.user(); + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); + if (propertyeExists) { + return user.settings.preferences[property] === value; + } else { + return defaultValue === true; + } + }, + highlights() { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences['highlights'] && user.settings.preferences['highlights'].join(', '); + }, + desktopNotificationEnabled() { + return KonchatNotification.notificationStatus.get() === 'granted' || (window.Notification && Notification.permission === 'granted'); + }, + desktopNotificationDisabled() { + return KonchatNotification.notificationStatus.get() === 'denied' || (window.Notification && Notification.permission === 'denied'); + }, + desktopNotificationDuration() { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.desktopNotificationDuration; + }, + showRoles() { + return RocketChat.settings.get('UI_DisplayRoles'); + } +}); + +Template.accountPreferences.onCreated(function() { + const settingsTemplate = this.parentTemplate(3); + if (settingsTemplate.child == null) { + settingsTemplate.child = []; + } + settingsTemplate.child.push(this); + const user = Meteor.user(); + if (user && user.settings && user.settings.preferences) { + this.useEmojis = new ReactiveVar(user.settings.preferences.desktopNotificationDuration == null || user.settings.preferences.useEmojis); + } + let instance = this; + this.autorun(() => { + if (instance.useEmojis && instance.useEmojis.get()) { + Tracker.afterFlush(() => $('#convertAsciiEmoji').show()); + } else { + Tracker.afterFlush(() => $('#convertAsciiEmoji').hide()); + } + }); + this.clearForm = function() { + this.find('#language').value = localStorage.getItem('userLanguage'); + }; + this.save = function() { + instance = this; + const data = {}; + let reload = false; + const selectedLanguage = $('#language').val(); + if (localStorage.getItem('userLanguage') !== selectedLanguage) { + localStorage.setItem('userLanguage', selectedLanguage); + data.language = selectedLanguage; + reload = true; + } + data.newRoomNotification = $('select[name=newRoomNotification]').val(); + data.newMessageNotification = $('select[name=newMessageNotification]').val(); + data.useEmojis = $('input[name=useEmojis]:checked').val(); + data.convertAsciiEmoji = $('input[name=convertAsciiEmoji]:checked').val(); + data.saveMobileBandwidth = $('input[name=saveMobileBandwidth]:checked').val(); + data.collapseMediaByDefault = $('input[name=collapseMediaByDefault]:checked').val(); + data.viewMode = parseInt($('#viewMode').find('select').val()); + data.hideUsernames = $('#hideUsernames').find('input:checked').val(); + data.hideRoles = $('#hideRoles').find('input:checked').val(); + data.hideFlexTab = $('#hideFlexTab').find('input:checked').val(); + data.hideAvatars = $('#hideAvatars').find('input:checked').val(); + data.mergeChannels = $('#mergeChannels').find('input:checked').val(); + data.sendOnEnter = $('#sendOnEnter').find('select').val(); + data.unreadRoomsMode = $('input[name=unreadRoomsMode]:checked').val(); + data.autoImageLoad = $('input[name=autoImageLoad]:checked').val(); + data.emailNotificationMode = $('select[name=emailNotificationMode]').val(); + data.highlights = _.compact(_.map($('[name=highlights]').val().split(','), function(e) { + return _.trim(e); + })); + data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val(); + data.unreadAlert = $('#unreadAlert').find('input:checked').val(); + Meteor.call('saveUserPreferences', data, function(error, results) { + if (results) { + toastr.success(t('Preferences_saved')); + instance.clearForm(); + if (reload) { + setTimeout(function() { + Meteor._reload.reload(); + }, 1000); + } + } + if (error) { + return handleError(error); + } + }); + }; +}); + +Template.accountPreferences.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); + +Template.accountPreferences.events({ + 'click .submit button'(e, t) { + t.save(); + }, + 'change input[name=useEmojis]'(e, t) { + t.useEmojis.set($(e.currentTarget).val() === '1'); + }, + 'click .enable-notifications'() { + KonchatNotification.getDesktopPermission(); + }, + 'click .test-notifications'() { + KonchatNotification.notify({ + duration: $('input[name=desktopNotificationDuration]').val(), + payload: { sender: { username: 'rocket.cat' } + }, + title: TAPi18n.__('Desktop_Notification_Test'), + text: TAPi18n.__('This_is_a_desktop_notification') + }); + }, + 'change .audio'(e) { + e.preventDefault(); + const audio = $(e.currentTarget).val(); + if (audio === 'none') { + return; + } + if (audio) { + const $audio = $(`audio#${ audio }`); + return $audio && $audio[0] && $audio.play(); + } + } +}); diff --git a/packages/rocketchat-ui-account/client/accountProfile.coffee b/packages/rocketchat-ui-account/client/accountProfile.coffee deleted file mode 100644 index d1e0979cb50046048f215eed555d97fe814bc0e8..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/accountProfile.coffee +++ /dev/null @@ -1,203 +0,0 @@ -import toastr from 'toastr' -Template.accountProfile.helpers - allowDeleteOwnAccount: -> - return RocketChat.settings.get('Accounts_AllowDeleteOwnAccount') - - realname: -> - return Meteor.user().name - - username: -> - return Meteor.user().username - - email: -> - return Meteor.user().emails?[0]?.address - - emailVerified: -> - return Meteor.user().emails?[0]?.verified - - allowUsernameChange: -> - return RocketChat.settings.get("Accounts_AllowUsernameChange") and RocketChat.settings.get("LDAP_Enable") isnt true - - allowEmailChange: -> - return RocketChat.settings.get("Accounts_AllowEmailChange") - - usernameChangeDisabled: -> - return t('Username_Change_Disabled') - - allowPasswordChange: -> - return RocketChat.settings.get("Accounts_AllowPasswordChange") - - passwordChangeDisabled: -> - return t('Password_Change_Disabled') - - customFields: -> - return Meteor.user().customFields - -Template.accountProfile.onCreated -> - settingsTemplate = this.parentTemplate(3) - settingsTemplate.child ?= [] - settingsTemplate.child.push this - - @clearForm = -> - @find('#password').value = '' - - @changePassword = (newPassword, callback) -> - instance = @ - if not newPassword - return callback() - - else - if !RocketChat.settings.get("Accounts_AllowPasswordChange") - toastr.remove(); - toastr.error t('Password_Change_Disabled') - instance.clearForm() - return - - @save = (typedPassword) -> - instance = @ - - data = { typedPassword: typedPassword } - - if _.trim($('#password').val()) and RocketChat.settings.get("Accounts_AllowPasswordChange") - data.newPassword = $('#password').val() - - if _.trim $('#realname').val() - data.realname = _.trim $('#realname').val() - - if _.trim($('#username').val()) isnt Meteor.user().username - if !RocketChat.settings.get("Accounts_AllowUsernameChange") - toastr.remove(); - toastr.error t('Username_Change_Disabled') - instance.clearForm() - return - else - data.username = _.trim $('#username').val() - - if _.trim($('#email').val()) isnt Meteor.user().emails?[0]?.address - if !RocketChat.settings.get("Accounts_AllowEmailChange") - toastr.remove(); - toastr.error t('Email_Change_Disabled') - instance.clearForm() - return - else - data.email = _.trim $('#email').val() - - customFields = {} - $('[data-customfield=true]').each () -> - customFields[this.name] = $(this).val() or '' - - Meteor.call 'saveUserProfile', data, customFields, (error, results) -> - if results - toastr.remove(); - toastr.success t('Profile_saved_successfully') - swal.close() - instance.clearForm() - - if error - toastr.remove(); - handleError(error) - -Template.accountProfile.onRendered -> - Tracker.afterFlush -> - # this should throw an error-template - FlowRouter.go("home") if !RocketChat.settings.get("Accounts_AllowUserProfileChange") - SideNav.setFlex "accountFlex" - SideNav.openFlex() - -Template.accountProfile.events - 'click .submit button': (e, instance) -> - user = Meteor.user() - reqPass = ((_.trim($('#email').val()) isnt user?.emails?[0]?.address) or _.trim($('#password').val())) and s.trim(user?.services?.password?.bcrypt) - unless reqPass - return instance.save() - - swal - title: t("Please_enter_your_password"), - text: t("For_your_security_you_must_enter_your_current_password_to_continue"), - type: "input", - inputType: "password", - showCancelButton: true, - closeOnConfirm: false, - confirmButtonText: t('Save'), - cancelButtonText: t('Cancel') - - , (typedPassword) => - if typedPassword - toastr.remove(); - toastr.warning(t("Please_wait_while_your_profile_is_being_saved")); - instance.save(SHA256(typedPassword)) - else - swal.showInputError(t("You_need_to_type_in_your_password_in_order_to_do_this")); - return false; - 'click .logoutOthers button': (event, templateInstance) -> - Meteor.logoutOtherClients (error) -> - if error - toastr.remove(); - handleError(error) - else - toastr.remove(); - toastr.success t('Logged_out_of_other_clients_successfully') - 'click .delete-account button': (e) -> - e.preventDefault(); - if s.trim Meteor.user()?.services?.password?.bcrypt - swal - title: t("Are_you_sure_you_want_to_delete_your_account"), - text: t("If_you_are_sure_type_in_your_password"), - type: "input", - inputType: "password", - showCancelButton: true, - closeOnConfirm: false, - confirmButtonText: t('Delete') - cancelButtonText: t('Cancel') - - , (typedPassword) => - if typedPassword - toastr.remove(); - toastr.warning(t("Please_wait_while_your_account_is_being_deleted")); - Meteor.call 'deleteUserOwnAccount', SHA256(typedPassword), (error, results) -> - if error - toastr.remove(); - swal.showInputError(t("Your_password_is_wrong")); - else - swal.close(); - else - swal.showInputError(t("You_need_to_type_in_your_password_in_order_to_do_this")); - return false; - else - swal - title: t("Are_you_sure_you_want_to_delete_your_account"), - text: t("If_you_are_sure_type_in_your_username"), - type: "input", - showCancelButton: true, - closeOnConfirm: false, - confirmButtonText: t('Delete') - cancelButtonText: t('Cancel') - - , (deleteConfirmation) => - if deleteConfirmation is Meteor.user()?.username - toastr.remove(); - toastr.warning(t("Please_wait_while_your_account_is_being_deleted")); - Meteor.call 'deleteUserOwnAccount', deleteConfirmation, (error, results) -> - if error - toastr.remove(); - swal.showInputError(t("Your_password_is_wrong")); - else - swal.close(); - else - swal.showInputError(t("You_need_to_type_in_your_username_in_order_to_do_this")); - return false; - - 'click #resend-verification-email': (e) -> - e.preventDefault() - - e.currentTarget.innerHTML = e.currentTarget.innerHTML + ' ...' - e.currentTarget.disabled = true - - Meteor.call 'sendConfirmationEmail', Meteor.user().emails?[0]?.address, (error, results) => - if results - toastr.success t('Verification_email_sent') - else if error - handleError(error) - - e.currentTarget.innerHTML = e.currentTarget.innerHTML.replace(' ...', '') - e.currentTarget.disabled = false diff --git a/packages/rocketchat-ui-account/client/accountProfile.js b/packages/rocketchat-ui-account/client/accountProfile.js new file mode 100644 index 0000000000000000000000000000000000000000..2038b340d2de071b9dc9ae128ae566f073dc39f8 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountProfile.js @@ -0,0 +1,233 @@ +import toastr from 'toastr'; +Template.accountProfile.helpers({ + allowDeleteOwnAccount() { + return RocketChat.settings.get('Accounts_AllowDeleteOwnAccount'); + }, + realname() { + return Meteor.user().name; + }, + username() { + return Meteor.user().username; + }, + email() { + const user = Meteor.user(); + return user.emails && user.emails[0] && user.emails[0].address; + }, + emailVerified() { + const user = Meteor.user(); + return user.emails && user.emails[0] && user.emails[0].verified; + }, + allowUsernameChange() { + return RocketChat.settings.get('Accounts_AllowUsernameChange') && RocketChat.settings.get('LDAP_Enable') !== true; + }, + allowEmailChange() { + return RocketChat.settings.get('Accounts_AllowEmailChange'); + }, + usernameChangeDisabled() { + return t('Username_Change_Disabled'); + }, + allowPasswordChange() { + return RocketChat.settings.get('Accounts_AllowPasswordChange'); + }, + passwordChangeDisabled() { + return t('Password_Change_Disabled'); + }, + customFields() { + return Meteor.user().customFields; + } +}); + +Template.accountProfile.onCreated(function() { + const settingsTemplate = this.parentTemplate(3); + if (settingsTemplate.child == null) { + settingsTemplate.child = []; + } + settingsTemplate.child.push(this); + this.clearForm = function() { + this.find('#password').value = ''; + }; + this.changePassword = function(newPassword, callback) { + const instance = this; + if (!newPassword) { + return callback(); + } else if (!RocketChat.settings.get('Accounts_AllowPasswordChange')) { + toastr.remove(); + toastr.error(t('Password_Change_Disabled')); + instance.clearForm(); + return; + } + }; + this.save = function(typedPassword) { + const instance = this; + const data = { + typedPassword + }; + if (_.trim($('#password').val()) && RocketChat.settings.get('Accounts_AllowPasswordChange')) { + data.newPassword = $('#password').val(); + } + if (_.trim($('#realname').val())) { + data.realname = _.trim($('#realname').val()); + } + if (_.trim($('#username').val()) !== Meteor.user().username) { + if (!RocketChat.settings.get('Accounts_AllowUsernameChange')) { + toastr.remove(); + toastr.error(t('Username_Change_Disabled')); + instance.clearForm(); + return; + } else { + data.username = _.trim($('#username').val()); + } + } + const user = Meteor.user(); + if (_.trim($('#email').val()) !== (user.emails && user.emails[0] && user.emails[0].address)) { + if (!RocketChat.settings.get('Accounts_AllowEmailChange')) { + toastr.remove(); + toastr.error(t('Email_Change_Disabled')); + instance.clearForm(); + return; + } else { + data.email = _.trim($('#email').val()); + } + } + const customFields = {}; + $('[data-customfield=true]').each(function() { + customFields[this.name] = $(this).val() || ''; + }); + Meteor.call('saveUserProfile', data, customFields, function(error, results) { + if (results) { + toastr.remove(); + toastr.success(t('Profile_saved_successfully')); + swal.close(); + instance.clearForm(); + } + if (error) { + toastr.remove(); + return handleError(error); + } + }); + }; +}); + +Template.accountProfile.onRendered(function() { + Tracker.afterFlush(function() { + if (!RocketChat.settings.get('Accounts_AllowUserProfileChange')) { + FlowRouter.go('home'); + } + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); + +Template.accountProfile.events({ + 'click .submit button'(e, instance) { + const user = Meteor.user(); + const reqPass = ((_.trim($('#email').val()) !== (user && user.emails && user.emails[0] && user.emails[0].address)) || _.trim($('#password').val())) && (user && user.services && user.services.password && s.trim(user.services.password.bcrypt)); + if (!reqPass) { + return instance.save(); + } + swal({ + title: t('Please_enter_your_password'), + text: t('For_your_security_you_must_enter_your_current_password_to_continue'), + type: 'input', + inputType: 'password', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Save'), + cancelButtonText: t('Cancel') + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); + instance.save(SHA256(typedPassword)); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } + }); + }, + 'click .logoutOthers button'() { + Meteor.logoutOtherClients(function(error) { + if (error) { + toastr.remove(); + handleError(error); + } else { + toastr.remove(); + toastr.success(t('Logged_out_of_other_clients_successfully')); + } + }); + }, + 'click .delete-account button'(e) { + e.preventDefault(); + const user = Meteor.user(); + if (s.trim(user && user.services && user.services.password && user.services.password.bcrypt)) { + swal({ + title: t('Are_you_sure_you_want_to_delete_your_account'), + text: t('If_you_are_sure_type_in_your_password'), + type: 'input', + inputType: 'password', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Delete'), + cancelButtonText: t('Cancel') + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { + if (error) { + toastr.remove(); + swal.showInputError(t('Your_password_is_wrong')); + } else { + swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } + }); + } else { + swal({ + title: t('Are_you_sure_you_want_to_delete_your_account'), + text: t('If_you_are_sure_type_in_your_username'), + type: 'input', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Delete'), + cancelButtonText: t('Cancel') + }, (deleteConfirmation) => { + const user = Meteor.user(); + if (deleteConfirmation === (user && user.username)) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { + if (error) { + toastr.remove(); + swal.showInputError(t('Your_password_is_wrong')); + } else { + swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); + return false; + } + }); + } + }, + 'click #resend-verification-email'(e) { + const user = Meteor.user(); + e.preventDefault(); + e.currentTarget.innerHTML = `${ e.currentTarget.innerHTML } ...`; + e.currentTarget.disabled = true; + Meteor.call('sendConfirmationEmail', user.emails && user.emails[0] && user.emails[0].address((error, results) => { + if (results) { + toastr.success(t('Verification_email_sent')); + } else if (error) { + handleError(error); + } + e.currentTarget.innerHTML = e.currentTarget.innerHTML.replace(' ...', ''); + return e.currentTarget.disabled = false; + })); + } +}); diff --git a/packages/rocketchat-ui-account/client/avatar/avatar.coffee b/packages/rocketchat-ui-account/client/avatar/avatar.coffee deleted file mode 100644 index b0abe8754a17e541350f55cda4e2643c23d25398..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/avatar/avatar.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Template.avatar.helpers - imageUrl: -> - username = this.username - if not username? and this.userId? - username = Meteor.users.findOne(this.userId)?.username - - if not username? - return - - Session.get "avatar_random_#{username}" - - url = getAvatarUrlFromUsername(username) - - return "background-image:url(#{url});" diff --git a/packages/rocketchat-ui-account/client/avatar/avatar.js b/packages/rocketchat-ui-account/client/avatar/avatar.js new file mode 100644 index 0000000000000000000000000000000000000000..74214467ccf50b1f8fb65e3a355fdf9bb8af81c7 --- /dev/null +++ b/packages/rocketchat-ui-account/client/avatar/avatar.js @@ -0,0 +1,15 @@ +Template.avatar.helpers({ + imageUrl() { + let username = this.username; + if (username == null && this.userId != null) { + const user = Meteor.users.findOne(this.userId); + username = user && user.username; + } + if (username == null) { + return; + } + Session.get(`avatar_random_${ username }`); + const url = getAvatarUrlFromUsername(username); + return `background-image:url(${ url });`; + } +}); diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.coffee b/packages/rocketchat-ui-account/client/avatar/prompt.coffee deleted file mode 100644 index 50f3d8838fdfe26815adf9f85e9a23b04781255f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/avatar/prompt.coffee +++ /dev/null @@ -1,105 +0,0 @@ -import toastr from 'toastr' -Template.avatarPrompt.onCreated -> - self = this - self.suggestions = new ReactiveVar - self.upload = new ReactiveVar - - self.getSuggestions = -> - self.suggestions.set undefined - Meteor.call 'getAvatarSuggestion', (error, avatars) -> - self.suggestions.set - ready: true - avatars: avatars - - self.getSuggestions() - -Template.avatarPrompt.onRendered -> - Tracker.afterFlush -> - # this should throw an error-template - FlowRouter.go("home") if !RocketChat.settings.get("Accounts_AllowUserAvatarChange") - SideNav.setFlex "accountFlex" - SideNav.openFlex() - -Template.avatarPrompt.helpers - suggestions: -> - return Template.instance().suggestions.get() - - suggestAvatar: (service) -> - suggestions = Template.instance().suggestions.get() - return RocketChat.settings.get("Accounts_OAuth_#{_.capitalize service}") and not suggestions.avatars[service] - - upload: -> - return Template.instance().upload.get() - - username: -> - return Meteor.user()?.username - - initialsUsername: -> - return '@'+Meteor.user()?.username - -Template.avatarPrompt.events - 'click .select-service': -> - if @service is 'initials' - Meteor.call 'resetAvatar', (err) -> - if err?.details?.timeToReset? - toastr.error t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) }) - else - toastr.success t('Avatar_changed_successfully') - RocketChat.callbacks.run('userAvatarSet', 'initials') - else if @service is 'url' - if _.trim $('#avatarurl').val() - Meteor.call 'setAvatarFromService', $('#avatarurl').val(), '', @service, (err) -> - if err - if err.details?.timeToReset? - toastr.error t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) }) - else - toastr.error t('Avatar_url_invalid_or_error') - else - toastr.success t('Avatar_changed_successfully') - RocketChat.callbacks.run('userAvatarSet', 'url') - else - toastr.error t('Please_enter_value_for_url') - else - tmpService = @service - Meteor.call 'setAvatarFromService', @blob, @contentType, @service, (err) -> - if err?.details?.timeToReset? - toastr.error t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) }) - else - toastr.success t('Avatar_changed_successfully') - RocketChat.callbacks.run('userAvatarSet', tmpService) - - 'click .login-with-service': (event, template) -> - loginWithService = "loginWith#{_.capitalize(this)}" - - serviceConfig = {} - - Meteor[loginWithService] serviceConfig, (error) -> - if error?.error is 'github-no-public-email' - alert t("github_no_public_email") - return - - console.log error - if error? - toastr.error error.message - return - - template.getSuggestions() - - 'change .avatar-file-input': (event, template) -> - e = event.originalEvent or event - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - for blob in files - if not /image\/.+/.test blob.type - return - - reader = new FileReader() - reader.readAsDataURL(blob) - reader.onloadend = -> - template.upload.set - service: 'upload' - contentType: blob.type - blob: reader.result - RocketChat.callbacks.run('userAvatarSet', 'upload') diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.js b/packages/rocketchat-ui-account/client/avatar/prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..b2a02bb5b8854a4e60d1271d444c53bcbd99cb9c --- /dev/null +++ b/packages/rocketchat-ui-account/client/avatar/prompt.js @@ -0,0 +1,129 @@ +import toastr from 'toastr'; +Template.avatarPrompt.onCreated(function() { + const self = this; + self.suggestions = new ReactiveVar; + self.upload = new ReactiveVar; + self.getSuggestions = function() { + self.suggestions.set(undefined); + Meteor.call('getAvatarSuggestion', function(error, avatars) { + self.suggestions.set({ ready: true, avatars }); + }); + }; + self.getSuggestions(); +}); + +Template.avatarPrompt.onRendered(function() { + Tracker.afterFlush(function() { + if (!RocketChat.settings.get('Accounts_AllowUserAvatarChange')) { + FlowRouter.go('home'); + } + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); + +Template.avatarPrompt.helpers({ + suggestions() { + return Template.instance().suggestions.get(); + }, + suggestAvatar(service) { + const suggestions = Template.instance().suggestions.get(); + return RocketChat.settings.get(`Accounts_OAuth_${ _.capitalize(service) }`) && !suggestions.avatars[service]; + }, + upload() { + return Template.instance().upload.get(); + }, + username() { + const user = Meteor.user(); + return user && user.username; + }, + initialsUsername() { + const user = Meteor.user(); + return `@${ user && user.username }`; + } +}); + +Template.avatarPrompt.events({ + 'click .select-service'() { + if (this.service === 'initials') { + Meteor.call('resetAvatar', function(err) { + if (err && err.details.timeToReset && err.details.timeToReset) { + toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', 'initials'); + } + }); + } else if (this.service === 'url') { + if (_.trim($('#avatarurl').val())) { + Meteor.call('setAvatarFromService', $('#avatarurl').val(), '', this.service, function(err) { + if (err) { + if (err.details.timeToReset && err.details.timeToReset) { + toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.error(t('Avatar_url_invalid_or_error')); + } + } else { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', 'url'); + } + }); + } else { + toastr.error(t('Please_enter_value_for_url')); + } + } else { + const tmpService = this.service; + Meteor.call('setAvatarFromService', this.blob, this.contentType, this.service, function(err) { + if (err && err.details.timeToReset && err.details.timeToReset) { + toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', tmpService); + } + }); + } + }, + 'click .login-with-service'(event, template) { + const loginWithService = `loginWith${ _.capitalize(this) }`; + const serviceConfig = {}; + Meteor[loginWithService](serviceConfig, function(error) { + if (error && error.error) { + if (error.error === 'github-no-public-email') { + return alert(t('github_no_public_email')); + } + console.log(error); + return toastr.error(error.message); + } + template.getSuggestions(); + }); + }, + 'change .avatar-file-input'(event, template) { + const e = event.originalEvent || event; + let files = e.target.files; + if (!files || files.length === 0) { + files = (e.dataTransfer && e.dataTransfer.files) || []; + } + Object.keys(files).forEach(key => { + const blob = files[key]; + if (!/image\/.+/.test(blob.type)) { + return; + } + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function() { + template.upload.set({ + service: 'upload', + contentType: blob.type, + blob: reader.result + }); + RocketChat.callbacks.run('userAvatarSet', 'upload'); + }; + }); + } +}); diff --git a/packages/rocketchat-ui-account/package.js b/packages/rocketchat-ui-account/package.js index 29966722f51b55ef5bbc9d3e255079ca26454ef4..b461ce56a88373206c334e56bd41339f48132cca 100644 --- a/packages/rocketchat-ui-account/package.js +++ b/packages/rocketchat-ui-account/package.js @@ -14,7 +14,6 @@ Package.onUse(function(api) { api.use([ 'ecmascript', 'templating', - 'coffeescript', 'underscore', 'rocketchat:lib', 'sha' @@ -27,12 +26,12 @@ Package.onUse(function(api) { api.addFiles('client/avatar/avatar.html', 'client'); api.addFiles('client/avatar/prompt.html', 'client'); - api.addFiles('client/account.coffee', 'client'); - api.addFiles('client/accountFlex.coffee', 'client'); - api.addFiles('client/accountPreferences.coffee', 'client'); - api.addFiles('client/accountProfile.coffee', 'client'); - api.addFiles('client/avatar/avatar.coffee', 'client'); - api.addFiles('client/avatar/prompt.coffee', 'client'); + api.addFiles('client/account.js', 'client'); + api.addFiles('client/accountFlex.js', 'client'); + api.addFiles('client/accountPreferences.js', 'client'); + api.addFiles('client/accountProfile.js', 'client'); + api.addFiles('client/avatar/avatar.js', 'client'); + api.addFiles('client/avatar/prompt.js', 'client'); // api.addAssets('styles/side-nav.less', 'client'); }); diff --git a/packages/rocketchat-ui-admin/client/adminInfo.coffee b/packages/rocketchat-ui-admin/client/adminInfo.coffee index 51cea0151143e0ea030a56660898f75a0c0f4049..1bfaedf4c899ec0fda22f0cc9d51990bd139fa6f 100644 --- a/packages/rocketchat-ui-admin/client/adminInfo.coffee +++ b/packages/rocketchat-ui-admin/client/adminInfo.coffee @@ -5,6 +5,8 @@ Template.adminInfo.helpers return Template.instance().ready.get() statistics: -> return Template.instance().statistics.get() + instances: -> + return Template.instance().instances.get() inGB: (size) -> if size > 1073741824 return _.numberFormat(size / 1024 / 1024 / 1024, 2) + ' GB' @@ -52,13 +54,20 @@ Template.adminInfo.onRendered -> Template.adminInfo.onCreated -> instance = @ @statistics = new ReactiveVar {} + @instances = new ReactiveVar [] @ready = new ReactiveVar false if RocketChat.authz.hasAllPermission('view-statistics') Meteor.call 'getStatistics', (error, statistics) -> - instance.ready.set true if error handleError(error) else instance.statistics.set statistics + Meteor.call 'instances/get', (error, instances) -> + instance.ready.set true + if error + handleError(error) + else + instance.instances.set instances + diff --git a/packages/rocketchat-ui-admin/client/adminInfo.html b/packages/rocketchat-ui-admin/client/adminInfo.html index e3d5358d08ebf8ff3b1168349e6ea421d504f01a..c900a7c6dac682b4b03ce3be339b1d69a33a759b 100644 --- a/packages/rocketchat-ui-admin/client/adminInfo.html +++ b/packages/rocketchat-ui-admin/client/adminInfo.html @@ -226,6 +226,50 @@ </tr> </table> + {{#if instances}} + <h3>{{_ "Broadcast_Connected_Instances"}}</h3> + {{#each instances}} + <table class="statistics-table secondary-background-color"> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Address"}}</th> + <td class="border-component-color">{{address}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Auth"}}</th> + <td class="border-component-color">{{broadcastAuth}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Current_Status"}} > {{_ "Connected"}}</th> + <td class="border-component-color">{{currentStatus.connected}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Current_Status"}} > {{_ "Retry_Count"}}</th> + <td class="border-component-color">{{currentStatus.retryCount}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Current_Status"}} > {{_ "Status"}}</th> + <td class="border-component-color">{{currentStatus.status}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "ID"}}</th> + <td class="border-component-color">{{instanceRecord._id}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "PID"}}</th> + <td class="border-component-color">{{instanceRecord.pid}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "Created_at"}}</th> + <td class="border-component-color">{{formatDate instanceRecord._createdAt}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "Updated_at"}}</th> + <td class="border-component-color">{{formatDate instanceRecord._updatedAt}}</td> + </tr> + </table> + {{/each}} + {{/if}} + <button type="button" class="button primary refresh">Refresh</button> {{else}} {{_ "Loading..."}} diff --git a/packages/rocketchat-ui-message/client/message.coffee b/packages/rocketchat-ui-message/client/message.coffee deleted file mode 100644 index 2684dfd9c0a3b4f60855e444d31b5198775f7672..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/message.coffee +++ /dev/null @@ -1,254 +0,0 @@ -import moment from 'moment' - -Template.message.helpers - encodeURI: (text) -> - return encodeURI(text) - isBot: -> - return 'bot' if this.bot? - roleTags: -> - if not RocketChat.settings.get('UI_DisplayRoles') or Meteor.user()?.settings?.preferences?.hideRoles - return [] - roles = _.union(UserRoles.findOne(this.u?._id)?.roles, RoomRoles.findOne({'u._id': this.u?._id, rid: this.rid })?.roles) - return RocketChat.models.Roles.find({ _id: { $in: roles }, description: { $exists: 1, $ne: '' } }, { fields: { description: 1 } }) - isGroupable: -> - return 'false' if this.groupable is false - isSequential: -> - return 'sequential' if this.groupable isnt false - avatarFromUsername: -> - if this.avatar? and this.avatar[0] is '@' - return this.avatar.replace(/^@/, '') - getEmoji: (emoji) -> - return renderEmoji emoji - getName: -> - if this.alias - return this.alias - if RocketChat.settings.get('UI_Use_Real_Name') and this.u?.name - return this.u.name - return this.u?.username - showUsername: -> - return this.alias or (RocketChat.settings.get('UI_Use_Real_Name') and this.u?.name) - own: -> - return 'own' if this.u?._id is Meteor.userId() - timestamp: -> - return +this.ts - chatops: -> - return 'chatops-message' if this.u?.username is RocketChat.settings.get('Chatops_Username') - time: -> - return moment(this.ts).format(RocketChat.settings.get('Message_TimeFormat')) - date: -> - return moment(this.ts).format(RocketChat.settings.get('Message_DateFormat')) - isTemp: -> - if @temp is true - return 'temp' - body: -> - return Template.instance().body - system: (returnClass) -> - if RocketChat.MessageTypes.isSystemMessage(this) - if returnClass - return 'color-info-font-color' - - return 'system' - - showTranslated: -> - if RocketChat.settings.get('AutoTranslate_Enabled') and this.u?._id isnt Meteor.userId() and !RocketChat.MessageTypes.isSystemMessage(this) - subscription = RocketChat.models.Subscriptions.findOne({ rid: this.rid, 'u._id': Meteor.userId() }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } }); - language = RocketChat.AutoTranslate.getLanguage(this.rid); - return this.autoTranslateFetching || (subscription?.autoTranslate isnt this.autoTranslateShowInverse && this.translations && this.translations[language]) # || _.find(this.attachments, (attachment) -> attachment.translations && attachment.translations[language] && attachment.author_name isnt Meteor.user().username ) - - edited: -> - return Template.instance().wasEdited - - editTime: -> - if Template.instance().wasEdited - return moment(@editedAt).format(RocketChat.settings.get('Message_DateFormat') + ' ' + RocketChat.settings.get('Message_TimeFormat')) - editedBy: -> - return "" unless Template.instance().wasEdited - # try to return the username of the editor, - # otherwise a special "?" character that will be - # rendered as a special avatar - return @editedBy?.username or "?" - canEdit: -> - hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', this.rid) - isEditAllowed = RocketChat.settings.get 'Message_AllowEditing' - editOwn = this.u?._id is Meteor.userId() - - return unless hasPermission or (isEditAllowed and editOwn) - - blockEditInMinutes = RocketChat.settings.get 'Message_AllowEditing_BlockEditInMinutes' - if blockEditInMinutes? and blockEditInMinutes isnt 0 - msgTs = moment(this.ts) if this.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockEditInMinutes - else - return true - - canDelete: -> - hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid ) - isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting') - deleteOwn = this.u?._id is Meteor.userId() - - return unless hasPermission or (isDeleteAllowed and deleteOwn) - - blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes' - if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 - msgTs = moment(this.ts) if this.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockDeleteInMinutes - else - return true - - showEditedStatus: -> - return RocketChat.settings.get 'Message_ShowEditedStatus' - label: -> - if @i18nLabel - return t(@i18nLabel) - else if @label - return @label - - hasOembed: -> - return false unless this.urls?.length > 0 and Template.oembedBaseWidget? and RocketChat.settings.get 'API_Embed' - - return false unless this.u?.username not in RocketChat.settings.get('API_EmbedDisabledFor')?.split(',').map (username) -> username.trim() - - return true - - reactions: -> - msgReactions = [] - userUsername = Meteor.user()?.username - - for emoji, reaction of @reactions - total = reaction.usernames.length - usernames = '@' + reaction.usernames.slice(0, 15).join(', @') - - usernames = usernames.replace('@'+userUsername, t('You').toLowerCase()) - - if total > 15 - usernames = usernames + ' ' + t('And_more', { length: total - 15 }).toLowerCase() - else - usernames = usernames.replace(/,([^,]+)$/, ' '+t('and')+'$1') - - if usernames[0] isnt '@' - usernames = usernames[0].toUpperCase() + usernames.substr(1) - - msgReactions.push - emoji: emoji - count: reaction.usernames.length - usernames: usernames - reaction: ' ' + t('Reacted_with').toLowerCase() + ' ' + emoji - userReacted: reaction.usernames.indexOf(userUsername) > -1 - - return msgReactions - - markUserReaction: (reaction) -> - if reaction.userReacted - return { - class: 'selected' - } - - hideReactions: -> - return 'hidden' if _.isEmpty(@reactions) - - actionLinks: -> - # remove 'method_id' and 'params' properties - return _.map(@actionLinks, (actionLink, key) -> _.extend({ id: key }, _.omit(actionLink, 'method_id', 'params'))) - - hideActionLinks: -> - return 'hidden' if _.isEmpty(@actionLinks) - - injectIndex: (data, index) -> - data.index = index - return - - hideCog: -> - subscription = RocketChat.models.Subscriptions.findOne({ rid: this.rid }); - return 'hidden' if not subscription? - - hideUsernames: -> - prefs = Meteor.user()?.settings?.preferences - return if prefs?.hideUsernames - -Template.message.onCreated -> - msg = Template.currentData() - - @wasEdited = msg.editedAt? and not RocketChat.MessageTypes.isSystemMessage(msg) - - @body = do -> - isSystemMessage = RocketChat.MessageTypes.isSystemMessage(msg) - messageType = RocketChat.MessageTypes.getType(msg) - if messageType?.render? - msg = messageType.render(msg) - else if messageType?.template? - # render template - else if messageType?.message? - if messageType.data?(msg)? - msg = TAPi18n.__(messageType.message, messageType.data(msg)) - else - msg = TAPi18n.__(messageType.message) - else - if msg.u?.username is RocketChat.settings.get('Chatops_Username') - msg.html = msg.msg - msg = RocketChat.callbacks.run 'renderMentions', msg - # console.log JSON.stringify message - msg = msg.html - else - msg = renderMessageBody msg - - if isSystemMessage - msg.html = RocketChat.Markdown.parse msg.html - - return msg - -Template.message.onViewRendered = (context) -> - view = this - this._domrange.onAttached (domRange) -> - currentNode = domRange.lastNode() - currentDataset = currentNode.dataset - previousNode = currentNode.previousElementSibling - nextNode = currentNode.nextElementSibling - $currentNode = $(currentNode) - $nextNode = $(nextNode) - - unless previousNode? - $currentNode.addClass('new-day').removeClass('sequential') - - else if previousNode?.dataset? - previousDataset = previousNode.dataset - previousMessageDate = new Date(parseInt(previousDataset.timestamp)) - currentMessageDate = new Date(parseInt(currentDataset.timestamp)) - - if previousMessageDate.toDateString() isnt currentMessageDate.toDateString() - $currentNode.addClass('new-day').removeClass('sequential') - else - $currentNode.removeClass('new-day') - - if previousDataset.groupable is 'false' or currentDataset.groupable is 'false' - $currentNode.removeClass('sequential') - else - if previousDataset.username isnt currentDataset.username or parseInt(currentDataset.timestamp) - parseInt(previousDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000 - $currentNode.removeClass('sequential') - else if not $currentNode.hasClass 'new-day' - $currentNode.addClass('sequential') - - if nextNode?.dataset? - nextDataset = nextNode.dataset - - if nextDataset.date isnt currentDataset.date - $nextNode.addClass('new-day').removeClass('sequential') - else - $nextNode.removeClass('new-day') - - if nextDataset.groupable isnt 'false' - if nextDataset.username isnt currentDataset.username or parseInt(nextDataset.timestamp) - parseInt(currentDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000 - $nextNode.removeClass('sequential') - else if not $nextNode.hasClass 'new-day' - $nextNode.addClass('sequential') - - if not nextNode? - templateInstance = if $('#chat-window-' + context.rid)[0] then Blaze.getView($('#chat-window-' + context.rid)[0])?.templateInstance() else null - - if currentNode.classList.contains('own') is true - templateInstance?.atBottom = true - else - if templateInstance?.firstNode && templateInstance?.atBottom is false - newMessage = templateInstance?.find(".new-message") - newMessage?.className = "new-message background-primary-action-color color-content-background-color " diff --git a/packages/rocketchat-ui-message/client/message.js b/packages/rocketchat-ui-message/client/message.js new file mode 100644 index 0000000000000000000000000000000000000000..ae7997d58303c72ab38cf424fc42e8f13264ef9a --- /dev/null +++ b/packages/rocketchat-ui-message/client/message.js @@ -0,0 +1,358 @@ +/* globals renderEmoji renderMessageBody*/ +import moment from 'moment'; + +Template.message.helpers({ + encodeURI(text) { + return encodeURI(text); + }, + isBot() { + if (this.bot != null) { + return 'bot'; + } + }, + roleTags() { + const user = Meteor.user(); + // test user -> settings -> preferences -> hideRoles + if (!RocketChat.settings.get('UI_DisplayRoles') || ['settings', 'preferences', 'hideRoles'].reduce((obj, field) => typeof obj !== 'undefined' && obj[field], user)) { + return []; + } + + if (!this.u || !this.u._id) { + return []; + } + /* globals UserRoles RoomRoles */ + const userRoles = UserRoles.findOne(this.u._id); + const roomRoles = RoomRoles.findOne({ + 'u._id': this.u._id, + rid: this.rid + }); + const roles = [...(userRoles && userRoles.roles) || [], ...(roomRoles && roomRoles.roles) || []]; + return RocketChat.models.Roles.find({ + _id: { + $in: roles + }, + description: { + $exists: 1, + $ne: '' + } + }, { + fields: { + description: 1 + } + }); + }, + isGroupable() { + if (this.groupable === false) { + return 'false'; + } + }, + isSequential() { + if (this.groupable !== false) { + return 'sequential'; + } + }, + avatarFromUsername() { + if ((this.avatar != null) && this.avatar[0] === '@') { + return this.avatar.replace(/^@/, ''); + } + }, + getEmoji(emoji) { + return renderEmoji(emoji); + }, + getName() { + if (this.alias) { + return this.alias; + } + if (!this.u) { + return ''; + } + return (RocketChat.settings.get('UI_Use_Real_Name') && this.u.name) || this.u.username; + }, + showUsername() { + return this.alias || RocketChat.settings.get('UI_Use_Real_Name') && this.u && this.u.name; + }, + own() { + if (this.u && this.u._id === Meteor.userId()) { + return 'own'; + } + }, + timestamp() { + return +this.ts; + }, + chatops() { + if (this.u && this.u.username === RocketChat.settings.get('Chatops_Username')) { + return 'chatops-message'; + } + }, + time() { + return moment(this.ts).format(RocketChat.settings.get('Message_TimeFormat')); + }, + date() { + return moment(this.ts).format(RocketChat.settings.get('Message_DateFormat')); + }, + isTemp() { + if (this.temp === true) { + return 'temp'; + } + }, + body() { + return Template.instance().body; + }, + system(returnClass) { + if (RocketChat.MessageTypes.isSystemMessage(this)) { + if (returnClass) { + return 'color-info-font-color'; + } + return 'system'; + } + }, + showTranslated() { + if (RocketChat.settings.get('AutoTranslate_Enabled') && this.u && this.u._id !== Meteor.userId() && !RocketChat.MessageTypes.isSystemMessage(this)) { + const subscription = RocketChat.models.Subscriptions.findOne({ + rid: this.rid, + 'u._id': Meteor.userId() + }, { + fields: { + autoTranslate: 1, + autoTranslateLanguage: 1 + } + }); + const language = RocketChat.AutoTranslate.getLanguage(this.rid); + return this.autoTranslateFetching || subscription && subscription.autoTranslate !== this.autoTranslateShowInverse && this.translations && this.translations[language]; + } + }, + edited() { + return Template.instance().wasEdited; + }, + editTime() { + if (Template.instance().wasEdited) { + return moment(this.editedAt).format(`${ RocketChat.settings.get('Message_DateFormat') } ${ RocketChat.settings.get('Message_TimeFormat') }`); + } + }, + editedBy() { + if (!Template.instance().wasEdited) { + return ''; + } + // try to return the username of the editor, + // otherwise a special "?" character that will be + // rendered as a special avatar + return (this.editedBy && this.editedBy.username) || '?'; + }, + canEdit() { + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', this.rid); + const isEditAllowed = RocketChat.settings.get('Message_AllowEditing'); + const editOwn = this.u && this.u._id === Meteor.userId(); + if (!(hasPermission || (isEditAllowed && editOwn))) { + return; + } + const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); + if (blockEditInMinutes) { + let msgTs; + if (this.ts != null) { + msgTs = moment(this.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockEditInMinutes; + } else { + return true; + } + }, + canDelete() { + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid); + const isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); + const deleteOwn = this.u && this.u._id === Meteor.userId(); + if (!(hasPermission || (isDeleteAllowed && deleteOwn))) { + return; + } + const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); + if (blockDeleteInMinutes) { + let msgTs; + if (this.ts != null) { + msgTs = moment(this.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockDeleteInMinutes; + } else { + return true; + } + }, + showEditedStatus() { + return RocketChat.settings.get('Message_ShowEditedStatus'); + }, + label() { + if (this.i18nLabel) { + return t(this.i18nLabel); + } else if (this.label) { + return this.label; + } + }, + hasOembed() { + if (!(this.urls && this.urls.length > 0 && Template.oembedBaseWidget != null && RocketChat.settings.get('API_Embed'))) { + return false; + } + if (!(RocketChat.settings.get('API_EmbedDisabledFor')||'').split(',').map(username => username.trim()).includes(this.u && this.u.username)) { + return false; + } + return true; + }, + reactions() { + const userUsername = Meteor.user().username; + return Object.keys(this.reactions||{}).map(emoji => { + const reaction = this.reactions[emoji]; + const total = reaction.usernames.length; + let usernames = `@${ reaction.usernames.slice(0, 15).join(', @') }`.replace(`@${ userUsername }`, t('You').toLowerCase()); + if (total > 15) { + usernames = `${ usernames } ${ t('And_more', { + length: total - 15 + }).toLowerCase() }`; + } else { + usernames = usernames.replace(/,([^,]+)$/, ` ${ t('and') }$1`); + } + if (usernames[0] !== '@') { + usernames = usernames[0].toUpperCase() + usernames.substr(1); + } + return { + emoji, + count: reaction.usernames.length, + usernames, + reaction: ` ${ t('Reacted_with').toLowerCase() } ${ emoji }`, + userReacted: reaction.usernames.indexOf(userUsername) > -1 + }; + }); + }, + markUserReaction(reaction) { + if (reaction.userReacted) { + return { + 'class': 'selected' + }; + } + }, + hideReactions() { + if (_.isEmpty(this.reactions)) { + return 'hidden'; + } + }, + actionLinks() { + // remove 'method_id' and 'params' properties + return _.map(this.actionLinks, function(actionLink, key) { + return _.extend({ + id: key + }, _.omit(actionLink, 'method_id', 'params')); + }); + }, + hideActionLinks() { + if (_.isEmpty(this.actionLinks)) { + return 'hidden'; + } + }, + injectIndex(data, index) { + data.index = index; + }, + hideCog() { + const subscription = RocketChat.models.Subscriptions.findOne({ + rid: this.rid + }); + if (subscription == null) { + return 'hidden'; + } + } +}); + +Template.message.onCreated(function() { + let msg = Template.currentData(); + + this.wasEdited = (msg.editedAt != null) && !RocketChat.MessageTypes.isSystemMessage(msg); + + return this.body = (() => { + const isSystemMessage = RocketChat.MessageTypes.isSystemMessage(msg); + const messageType = RocketChat.MessageTypes.getType(msg)||{}; + if (messageType.render) { + msg = messageType.render(msg); + } else if (messageType.template) { + // render template + } else if (messageType.message) { + if (typeof messageType.data === 'function' && messageType.data(msg)) { + msg = TAPi18n.__(messageType.message, messageType.data(msg)); + } else { + msg = TAPi18n.__(messageType.message); + } + } else if (msg.u && msg.u.username === RocketChat.settings.get('Chatops_Username')) { + msg.html = msg.msg; + msg = RocketChat.callbacks.run('renderMentions', msg); + // console.log JSON.stringify message + msg = msg.html; + } else { + msg = renderMessageBody(msg); + } + + if (isSystemMessage) { + msg.html = RocketChat.Markdown.parse(msg.html); + } + return msg; + })(); +}); + +Template.message.onViewRendered = function(context) { + return this._domrange.onAttached(function(domRange) { + const currentNode = domRange.lastNode(); + const currentDataset = currentNode.dataset; + const previousNode = currentNode.previousElementSibling; + const nextNode = currentNode.nextElementSibling; + const $currentNode = $(currentNode); + const $nextNode = $(nextNode); + if (previousNode == null) { + $currentNode.addClass('new-day').removeClass('sequential'); + } else if (previousNode.dataset) { + const previousDataset = previousNode.dataset; + const previousMessageDate = new Date(parseInt(previousDataset.timestamp)); + const currentMessageDate = new Date(parseInt(currentDataset.timestamp)); + if (previousMessageDate.toDateString() !== currentMessageDate.toDateString()) { + $currentNode.addClass('new-day').removeClass('sequential'); + } else { + $currentNode.removeClass('new-day'); + } + if (previousDataset.groupable === 'false' || currentDataset.groupable === 'false') { + $currentNode.removeClass('sequential'); + } else if (previousDataset.username !== currentDataset.username || parseInt(currentDataset.timestamp) - parseInt(previousDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000) { + $currentNode.removeClass('sequential'); + } else if (!$currentNode.hasClass('new-day')) { + $currentNode.addClass('sequential'); + } + } + if (nextNode && nextNode.dataset) { + const nextDataset = nextNode.dataset; + if (nextDataset.date !== currentDataset.date) { + $nextNode.addClass('new-day').removeClass('sequential'); + } else { + $nextNode.removeClass('new-day'); + } + if (nextDataset.groupable !== 'false') { + if (nextDataset.username !== currentDataset.username || parseInt(nextDataset.timestamp) - parseInt(currentDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000) { + $nextNode.removeClass('sequential'); + } else if (!$nextNode.hasClass('new-day')) { + $nextNode.addClass('sequential'); + } + } + } + if (nextNode == null) { + const [el] = $(`#chat-window-${ context.rid }`); + const view = el && Blaze.getView(el); + const templateInstance = view && view.templateInstance(); + if (!templateInstance) { + return; + } + if (currentNode.classList.contains('own') === true) { + return (templateInstance.atBottom = true); + } else if (templateInstance.firstNode && templateInstance.atBottom === false) { + const newMessage = templateInstance.find('.new-message'); + return newMessage && (newMessage.className = 'new-message background-primary-action-color color-content-background-color '); + } + } + }); +}; diff --git a/packages/rocketchat-ui-message/client/messageBox.coffee b/packages/rocketchat-ui-message/client/messageBox.coffee deleted file mode 100644 index 2587fda0c301ecbd128e41861c4d0fd4e683bed1..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/messageBox.coffee +++ /dev/null @@ -1,391 +0,0 @@ -import toastr from 'toastr' -import mime from 'mime-type/with-db' -import moment from 'moment' -import {VRecDialog} from 'meteor/rocketchat:ui-vrecord' - -katexSyntax = -> - if RocketChat.katex.katex_enabled() - return "$$KaTeX$$" if RocketChat.katex.dollar_syntax_enabled() - return "\\[KaTeX\\]" if RocketChat.katex.parenthesis_syntax_enabled() - - return false - -Template.messageBox.helpers - roomName: -> - roomData = Session.get('roomData' + this._id) - return '' unless roomData - - if roomData.t is 'd' - return ChatSubscription.findOne({ rid: this._id }, { fields: { name: 1 } })?.name - else - return roomData.name - showMarkdown: -> - return RocketChat.Markdown - showMarkdownCode: -> - return RocketChat.MarkdownCode - showKatex: -> - return RocketChat.katex - katexSyntax: -> - return katexSyntax() - showFormattingTips: -> - return RocketChat.settings.get('Message_ShowFormattingTips') and (RocketChat.Markdown or RocketChat.MarkdownCode or katexSyntax()) - canJoin: -> - return Meteor.userId()? and RocketChat.roomTypes.verifyShowJoinLink @_id - joinCodeRequired: -> - return Session.get('roomData' + this._id)?.joinCodeRequired - subscribed: -> - return RocketChat.roomTypes.verifyCanSendMessage @_id - allowedToSend: -> - if RocketChat.roomTypes.readOnly @_id, Meteor.user() - return false - - if RocketChat.roomTypes.archived @_id - return false - - roomData = Session.get('roomData' + this._id) - if roomData?.t is 'd' - subscription = ChatSubscription.findOne({ rid: this._id }, { fields: { archived: 1, blocked: 1, blocker: 1 } }) - if subscription and (subscription.archived or subscription.blocked or subscription.blocker) - return false - - return true - isBlockedOrBlocker: -> - roomData = Session.get('roomData' + this._id) - if roomData?.t is 'd' - subscription = ChatSubscription.findOne({ rid: this._id }, { fields: { blocked: 1, blocker: 1 } }) - if subscription and (subscription.blocked or subscription.blocker) - return true - - getPopupConfig: -> - template = Template.instance() - return { - getInput: -> - return template.find('.input-message') - } - usersTyping: -> - users = MsgTyping.get @_id - if users.length is 0 - return - if users.length is 1 - return { - multi: false - selfTyping: MsgTyping.selfTyping.get() - users: users[0] - } - # usernames = _.map messages, (message) -> return message.u.username - last = users.pop() - if users.length > 4 - last = t('others') - # else - usernames = users.join(', ') - usernames = [usernames, last] - return { - multi: true - selfTyping: MsgTyping.selfTyping.get() - users: usernames.join " #{t 'and'} " - } - - groupAttachHidden: -> - return 'hidden' if RocketChat.settings.get('Message_Attachments_GroupAttach') - - fileUploadEnabled: -> - return RocketChat.settings.get('FileUpload_Enabled') - - fileUploadAllowedMediaTypes: -> - return RocketChat.settings.get('FileUpload_MediaTypeWhiteList') - - showFileUpload: -> - if (RocketChat.settings.get('FileUpload_Enabled')) - roomData = Session.get('roomData' + this._id) - if roomData?.t is 'd' - return RocketChat.settings.get('FileUpload_Enabled_Direct') - else - return true - else - return RocketChat.settings.get('FileUpload_Enabled') - - - showMic: -> - return Template.instance().showMicButton.get() - - showVRec: -> - return Template.instance().showVideoRec.get() - - showSend: -> - if not Template.instance().isMessageFieldEmpty.get() - return 'show-send' - - showLocation: -> - return RocketChat.Geolocation.get() isnt false - - notSubscribedTpl: -> - return RocketChat.roomTypes.getNotSubscribedTpl @_id - - showSandstorm: -> - return Meteor.settings.public.sandstorm && !Meteor.isCordova - - anonymousRead: -> - return not Meteor.userId()? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is true - - anonymousWrite: -> - return not Meteor.userId()? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is true and RocketChat.settings.get('Accounts_AllowAnonymousWrite') is true - -firefoxPasteUpload = (fn) -> - user = navigator.userAgent.match(/Firefox\/(\d+)\.\d/) - if !user or user[1] > 49 - return fn - return (event, instance) -> - if (event.originalEvent.ctrlKey or event.originalEvent.metaKey) and (event.keyCode == 86) - textarea = instance.find("textarea") - selectionStart = textarea.selectionStart - selectionEnd = textarea.selectionEnd - contentEditableDiv = instance.find('#msg_contenteditable') - contentEditableDiv.focus() - Meteor.setTimeout -> - pastedImg = contentEditableDiv.querySelector 'img' - textareaContent = textarea.value - startContent = textareaContent.substring(0, selectionStart) - endContent = textareaContent.substring(selectionEnd) - restoreSelection = (pastedText) -> - textarea.value = startContent + pastedText + endContent - textarea.selectionStart = selectionStart + pastedText.length - textarea.selectionEnd = textarea.selectionStart - contentEditableDiv.innerHTML = '' if pastedImg - textarea.focus - return if (!pastedImg || contentEditableDiv.innerHTML.length > 0) - [].slice.call(contentEditableDiv.querySelectorAll("br")).forEach (el) -> - contentEditableDiv.replaceChild(new Text("\n") , el) - restoreSelection(contentEditableDiv.innerText) - imageSrc = pastedImg.getAttribute("src") - if imageSrc.match(/^data:image/) - fetch(imageSrc) - .then((img)-> - return img.blob()) - .then (blob)-> - fileUpload [{ - file: blob - name: 'Clipboard' - }] - , 150 - fn?.apply @, arguments - - -Template.messageBox.events - 'click .join': (event) -> - event.stopPropagation() - event.preventDefault() - Meteor.call 'joinRoom', @_id, Template.instance().$('[name=joinCode]').val(), (err) => - if err? - toastr.error t(err.reason) - - if RocketChat.authz.hasAllPermission('preview-c-room') is false and RoomHistoryManager.getRoom(@_id).loaded is 0 - RoomManager.getOpenedRoomByRid(@_id).streamActive = false - RoomManager.getOpenedRoomByRid(@_id).ready = false - RoomHistoryManager.getRoom(@_id).loaded = undefined - RoomManager.computation.invalidate() - - 'click .register': (event) -> - event.stopPropagation() - event.preventDefault() - Session.set('forceLogin', true) - - 'click .register-anonymous': (event) -> - event.stopPropagation() - event.preventDefault() - - Meteor.call 'registerUser', {}, (error, loginData) -> - if loginData && loginData.token - Meteor.loginWithToken loginData.token - - - 'focus .input-message': (event, instance) -> - KonchatNotification.removeRoomNotification @_id - chatMessages[@_id].input = instance.find('.input-message') - - 'click .send-button': (event, instance) -> - input = instance.find('.input-message') - chatMessages[@_id].send(@_id, input, => - # fixes https://github.com/RocketChat/Rocket.Chat/issues/3037 - # at this point, the input is cleared and ready for autogrow - input.updateAutogrow() - instance.isMessageFieldEmpty.set(chatMessages[@_id].isEmpty()) - ) - input.focus() - - 'keyup .input-message': (event, instance) -> - chatMessages[@_id].keyup(@_id, event, instance) - instance.isMessageFieldEmpty.set(chatMessages[@_id].isEmpty()) - - 'paste .input-message': (e, instance) -> - Meteor.setTimeout -> - input = instance.find('.input-message') - input.updateAutogrow?() - , 50 - - if not e.originalEvent.clipboardData? - return - items = e.originalEvent.clipboardData.items - files = [] - for item in items - if item.kind is 'file' and item.type.indexOf('image/') isnt -1 - e.preventDefault() - files.push - file: item.getAsFile() - name: 'Clipboard - ' + moment().format(RocketChat.settings.get('Message_TimeAndDateFormat')) - - if files.length - fileUpload files - else - instance.isMessageFieldEmpty.set(false) - - 'keydown .input-message': firefoxPasteUpload((event, instance) -> - chatMessages[@_id].keydown(@_id, event, Template.instance())) - - 'input .input-message': (event) -> - chatMessages[@_id].valueChanged(@_id, event, Template.instance()) - - 'propertychange .input-message': (event) -> - if event.originalEvent.propertyName is 'value' - chatMessages[@_id].valueChanged(@_id, event, Template.instance()) - - "click .editing-commands-cancel > button": (e) -> - chatMessages[@_id].clearEditing() - - "click .editing-commands-save > button": (e) -> - chatMessages[@_id].send(@_id, chatMessages[@_id].input) - - 'change .message-form input[type=file]': (event, template) -> - e = event.originalEvent or event - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - filesToUpload = [] - for file in files - # `file.type = mime.lookup(file.name)` does not work. - Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }) - filesToUpload.push - file: file - name: file.name - - fileUpload filesToUpload - - "click .message-buttons.share": (e, t) -> - t.$('.share-items').toggleClass('hidden') - t.$('.message-buttons.share').toggleClass('active') - - 'click .message-form .message-buttons.location': (event, instance) -> - roomId = @_id - - position = RocketChat.Geolocation.get() - - latitude = position.coords.latitude - longitude = position.coords.longitude - - text = """ - <div class="location-preview"> - <img style="height: 250px; width: 250px;" src="https://maps.googleapis.com/maps/api/staticmap?zoom=14&size=250x250&markers=color:gray%7Clabel:%7C#{latitude},#{longitude}&key=#{RocketChat.settings.get('MapView_GMapsAPIKey')}" /> - </div> - """ - - swal - title: t('Share_Location_Title') - text: text - showCancelButton: true - closeOnConfirm: true - closeOnCancel: true - html: true - , (isConfirm) -> - if isConfirm isnt true - return - - Meteor.call "sendMessage", - _id: Random.id() - rid: roomId - msg: "" - location: - type: 'Point' - coordinates: [ longitude, latitude ] - - - 'click .message-form .mic': (e, t) -> - AudioRecorder.start -> - t.$('.stop-mic').removeClass('hidden') - t.$('.mic').addClass('hidden') - - 'click .message-form .video-button': (e, t) -> - if VRecDialog.opened - VRecDialog.close() - else - VRecDialog.open(e.currentTarget) - - 'click .message-form .stop-mic': (e, t) -> - AudioRecorder.stop (blob) -> - fileUpload [{ - file: blob - type: 'audio' - name: TAPi18n.__('Audio record') + '.wav' - }] - - t.$('.stop-mic').addClass('hidden') - t.$('.mic').removeClass('hidden') - - 'click .sandstorm-offer': (e, t) -> - roomId = @_id - RocketChat.Sandstorm.request "uiView", (err, data) => - if err or !data.token - console.error err - return - Meteor.call "sandstormClaimRequest", data.token, data.descriptor, (err, viewInfo) => - if err - console.error err - return - - Meteor.call "sendMessage", { - _id: Random.id() - rid: roomId - msg: "" - urls: [{ url: "grain://sandstorm", sandstormViewInfo: viewInfo }] - } - -Template.messageBox.onCreated -> - @isMessageFieldEmpty = new ReactiveVar true - @showMicButton = new ReactiveVar false - @showVideoRec = new ReactiveVar false - - @autorun => - videoRegex = /video\/webm|video\/\*/i - videoEnabled = !RocketChat.settings.get("FileUpload_MediaTypeWhiteList") || RocketChat.settings.get("FileUpload_MediaTypeWhiteList").match(videoRegex) - if RocketChat.settings.get('Message_VideoRecorderEnabled') and (navigator.getUserMedia? or navigator.webkitGetUserMedia?) and videoEnabled and RocketChat.settings.get('FileUpload_Enabled') - @showVideoRec.set true - else - @showVideoRec.set false - - wavRegex = /audio\/wav|audio\/\*/i - wavEnabled = !RocketChat.settings.get("FileUpload_MediaTypeWhiteList") || RocketChat.settings.get("FileUpload_MediaTypeWhiteList").match(wavRegex) - if RocketChat.settings.get('Message_AudioRecorderEnabled') and (navigator.getUserMedia? or navigator.webkitGetUserMedia?) and wavEnabled and RocketChat.settings.get('FileUpload_Enabled') - @showMicButton.set true - else - @showMicButton.set false - - -Meteor.startup -> - RocketChat.Geolocation = new ReactiveVar false - - Tracker.autorun -> - if RocketChat.settings.get('MapView_Enabled') is true and RocketChat.settings.get('MapView_GMapsAPIKey')?.length and navigator.geolocation?.getCurrentPosition? - success = (position) => - RocketChat.Geolocation.set position - - error = (error) => - console.log 'Error getting your geolocation', error - RocketChat.Geolocation.set false - - options = - enableHighAccuracy: true - maximumAge: 0 - timeout: 10000 - - navigator.geolocation.watchPosition success, error - else - RocketChat.Geolocation.set false diff --git a/packages/rocketchat-ui-message/client/messageBox.js b/packages/rocketchat-ui-message/client/messageBox.js new file mode 100644 index 0000000000000000000000000000000000000000..add30a122142c3afacbf892c932821e54e346d0a --- /dev/null +++ b/packages/rocketchat-ui-message/client/messageBox.js @@ -0,0 +1,454 @@ +/* globals fileUpload AudioRecorder KonchatNotification chatMessages */ +import toastr from 'toastr'; + +import mime from 'mime-type/with-db'; + +import moment from 'moment'; + +import {VRecDialog} from 'meteor/rocketchat:ui-vrecord'; + +function katexSyntax() { + if (RocketChat.katex.katex_enabled()) { + if (RocketChat.katex.dollar_syntax_enabled()) { + return '$$KaTeX$$'; + } + if (RocketChat.katex.parenthesis_syntax_enabled()) { + return '\\[KaTeX\\]'; + } + } + return false; +} + +Template.messageBox.helpers({ + roomName() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { + return ''; + } + if (roomData.t === 'd') { + const chat = ChatSubscription.findOne({ + rid: this._id + }, { + fields: { + name: 1 + } + }); + return chat && chat.name; + } else { + return roomData.name; + } + }, + showMarkdown() { + return RocketChat.Markdown; + }, + showMarkdownCode() { + return RocketChat.MarkdownCode; + }, + showKatex() { + return RocketChat.katex; + }, + katexSyntax() { + return katexSyntax(); + }, + showFormattingTips() { + return RocketChat.settings.get('Message_ShowFormattingTips') && (RocketChat.Markdown || RocketChat.MarkdownCode || katexSyntax()); + }, + canJoin() { + return RocketChat.roomTypes.verifyShowJoinLink(this._id); + }, + joinCodeRequired() { + const code = Session.get(`roomData${ this._id }`); + return code && code.joinCodeRequired; + }, + subscribed() { + return RocketChat.roomTypes.verifyCanSendMessage(this._id); + }, + allowedToSend() { + if (RocketChat.roomTypes.readOnly(this._id, Meteor.user())) { + return false; + } + if (RocketChat.roomTypes.archived(this._id)) { + return false; + } + const roomData = Session.get(`roomData${ this._id }`); + if (roomData && roomData.t === 'd') { + const subscription = ChatSubscription.findOne({ + rid: this._id + }, { + fields: { + archived: 1, + blocked: 1, + blocker: 1 + } + }); + if (subscription && (subscription.archived || subscription.blocked || subscription.blocker)) { + return false; + } + } + return true; + }, + isBlockedOrBlocker() { + const roomData = Session.get(`roomData${ this._id }`); + if (roomData && roomData.t === 'd') { + const subscription = ChatSubscription.findOne({ + rid: this._id + }, { + fields: { + blocked: 1, + blocker: 1 + } + }); + if (subscription && (subscription.blocked || subscription.blocker)) { + return true; + } + } + }, + getPopupConfig() { + const template = Template.instance(); + return { + getInput() { + return template.find('.input-message'); + } + }; + }, + /* globals MsgTyping*/ + usersTyping() { + const users = MsgTyping.get(this._id); + if (users.length === 0) { + return; + } + if (users.length === 1) { + return { + multi: false, + selfTyping: MsgTyping.selfTyping.get(), + users: users[0] + }; + } + let last = users.pop(); + if (users.length > 4) { + last = t('others'); + } + let usernames = users.join(', '); + usernames = [usernames, last]; + return { + multi: true, + selfTyping: MsgTyping.selfTyping.get(), + users: usernames.join(` ${ t('and') } `) + }; + }, + groupAttachHidden() { + if (RocketChat.settings.get('Message_Attachments_GroupAttach')) { + return 'hidden'; + } + }, + fileUploadEnabled() { + return RocketChat.settings.get('FileUpload_Enabled'); + }, + fileUploadAllowedMediaTypes() { + return RocketChat.settings.get('FileUpload_MediaTypeWhiteList'); + }, + showFileUpload() { + let roomData; + if (RocketChat.settings.get('FileUpload_Enabled')) { + roomData = Session.get(`roomData${ this._id }`); + if (roomData && roomData.t === 'd') { + return RocketChat.settings.get('FileUpload_Enabled_Direct'); + } else { + return true; + } + } else { + return RocketChat.settings.get('FileUpload_Enabled'); + } + }, + showMic() { + return Template.instance().showMicButton.get(); + }, + showVRec() { + return Template.instance().showVideoRec.get(); + }, + showSend() { + if (!Template.instance().isMessageFieldEmpty.get()) { + return 'show-send'; + } + }, + showLocation() { + return RocketChat.Geolocation.get() !== false; + }, + notSubscribedTpl() { + return RocketChat.roomTypes.getNotSubscribedTpl(this._id); + }, + showSandstorm() { + return Meteor.settings['public'].sandstorm && !Meteor.isCordova; + } +}); + +function firefoxPasteUpload(fn) { + const user = navigator.userAgent.match(/Firefox\/(\d+)\.\d/); + if (!user || user[1] > 49) { + return fn; + } + return function(event, instance) { + if ((event.originalEvent.ctrlKey || event.originalEvent.metaKey) && (event.keyCode === 86)) { + const textarea = instance.find('textarea'); + const {selectionStart, selectionEnd} = textarea; + const contentEditableDiv = instance.find('#msg_contenteditable'); + contentEditableDiv.focus(); + Meteor.setTimeout(function() { + const pastedImg = contentEditableDiv.querySelector('img'); + const textareaContent = textarea.value; + const startContent = textareaContent.substring(0, selectionStart); + const endContent = textareaContent.substring(selectionEnd); + const restoreSelection = function(pastedText) { + textarea.value = startContent + pastedText + endContent; + textarea.selectionStart = selectionStart + pastedText.length; + return textarea.selectionEnd = textarea.selectionStart; + }; + if (pastedImg) { + contentEditableDiv.innerHTML = ''; + } + textarea.focus; + if (!pastedImg || contentEditableDiv.innerHTML.length > 0) { + return [].slice.call(contentEditableDiv.querySelectorAll('br')).forEach(function(el) { + contentEditableDiv.replaceChild(new Text('\n'), el); + return restoreSelection(contentEditableDiv.innerText); + }); + } + const imageSrc = pastedImg.getAttribute('src'); + if (imageSrc.match(/^data:image/)) { + return fetch(imageSrc).then(function(img) { + return img.blob(); + }).then(function(blob) { + return fileUpload([ + { + file: blob, + name: 'Clipboard' + } + ]); + }); + } + }, 150); + } + return fn && fn.apply(this, arguments); + }; +} + +Template.messageBox.events({ + 'click .join'(event) { + event.stopPropagation(); + event.preventDefault(); + Meteor.call('joinRoom', this._id, Template.instance().$('[name=joinCode]').val(), (err) => { + if (err != null) { + toastr.error(t(err.reason)); + } + if (RocketChat.authz.hasAllPermission('preview-c-room') === false && RoomHistoryManager.getRoom(this._id).loaded === 0) { + RoomManager.getOpenedRoomByRid(this._id).streamActive = false; + RoomManager.getOpenedRoomByRid(this._id).ready = false; + RoomHistoryManager.getRoom(this._id).loaded = null; + RoomManager.computation.invalidate(); + } + }); + }, + 'focus .input-message'(event, instance) { + KonchatNotification.removeRoomNotification(this._id); + chatMessages[this._id].input = instance.find('.input-message'); + }, + 'click .send-button'(event, instance) { + const input = instance.find('.input-message'); + chatMessages[this._id].send(this._id, input, () => { + // fixes https://github.com/RocketChat/Rocket.Chat/issues/3037 + // at this point, the input is cleared and ready for autogrow + input.updateAutogrow(); + return instance.isMessageFieldEmpty.set(chatMessages[this._id].isEmpty()); + }); + return input.focus(); + }, + 'keyup .input-message'(event, instance) { + chatMessages[this._id].keyup(this._id, event, instance); + return instance.isMessageFieldEmpty.set(chatMessages[this._id].isEmpty()); + }, + 'paste .input-message'(e, instance) { + Meteor.setTimeout(function() { + const input = instance.find('.input-message'); + return typeof input.updateAutogrow === 'function' && input.updateAutogrow(); + }, 50); + if (e.originalEvent.clipboardData == null) { + return; + } + const items = [...e.originalEvent.clipboardData.items]; + const files = items.map(item => { + if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { + e.preventDefault(); + return { + file: item.getAsFile(), + name: `Clipboard - ${ moment().format(RocketChat.settings.get('Message_TimeAndDateFormat')) }` + }; + } + }).filter(e => e); + if (files.length) { + return fileUpload(files); + } else { + return instance.isMessageFieldEmpty.set(false); + } + }, + 'keydown .input-message': firefoxPasteUpload(function(event) { + return chatMessages[this._id].keydown(this._id, event, Template.instance()); + }), + 'input .input-message'(event) { + return chatMessages[this._id].valueChanged(this._id, event, Template.instance()); + }, + 'propertychange .input-message'(event) { + if (event.originalEvent.propertyName === 'value') { + return chatMessages[this._id].valueChanged(this._id, event, Template.instance()); + } + }, + 'click .editing-commands-cancel > button'() { + return chatMessages[this._id].clearEditing(); + }, + 'click .editing-commands-save > button'() { + return chatMessages[this._id].send(this._id, chatMessages[this._id].input); + }, + 'change .message-form input[type=file]'(event) { + const e = event.originalEvent || event; + let files = e.target.files; + if (!files || files.length === 0) { + files = (e.dataTransfer && e.dataTransfer.files) || []; + } + const filesToUpload = [...files].map(file => { + // `file.type = mime.lookup(file.name)` does not work. + Object.defineProperty(file, 'type', { + value: mime.lookup(file.name) + }); + return { + file, + name: file.name + }; + }); + return fileUpload(filesToUpload); + }, + 'click .message-buttons.share'(e, t) { + t.$('.share-items').toggleClass('hidden'); + return t.$('.message-buttons.share').toggleClass('active'); + }, + 'click .message-form .message-buttons.location'() { + const roomId = this._id; + const position = RocketChat.Geolocation.get(); + const latitude = position.coords.latitude; + const longitude = position.coords.longitude; + const text = `<div class="location-preview">\n <img style="height: 250px; width: 250px;" src="https://maps.googleapis.com/maps/api/staticmap?zoom=14&size=250x250&markers=color:gray%7Clabel:%7C${ latitude },${ longitude }&key=${ RocketChat.settings.get('MapView_GMapsAPIKey') }" />\n</div>`; + return swal({ + title: t('Share_Location_Title'), + text, + showCancelButton: true, + closeOnConfirm: true, + closeOnCancel: true, + html: true + }, function(isConfirm) { + if (isConfirm !== true) { + return; + } + return Meteor.call('sendMessage', { + _id: Random.id(), + rid: roomId, + msg: '', + location: { + type: 'Point', + coordinates: [longitude, latitude] + } + }); + }); + }, + 'click .message-form .mic'(e, t) { + return AudioRecorder.start(function() { + t.$('.stop-mic').removeClass('hidden'); + return t.$('.mic').addClass('hidden'); + }); + }, + 'click .message-form .video-button'(e) { + return VRecDialog.opened ? VRecDialog.close() : VRecDialog.open(e.currentTarget); + }, + 'click .message-form .stop-mic'(e, t) { + AudioRecorder.stop(function(blob) { + return fileUpload([ + { + file: blob, + type: 'audio', + name: `${ TAPi18n.__('Audio record') }.wav` + } + ]); + }); + t.$('.stop-mic').addClass('hidden'); + return t.$('.mic').removeClass('hidden'); + }, + 'click .sandstorm-offer'() { + const roomId = this._id; + return RocketChat.Sandstorm.request('uiView', (err, data) => { + if (err || !data.token) { + console.error(err); + return; + } + return Meteor.call('sandstormClaimRequest', data.token, data.descriptor, function(err, viewInfo) { + if (err) { + console.error(err); + return; + } + Meteor.call('sendMessage', { + _id: Random.id(), + rid: roomId, + msg: '', + urls: [ + { + url: 'grain://sandstorm', + sandstormViewInfo: viewInfo + } + ] + }); + }); + }); + } +}); + +Template.messageBox.onCreated(function() { + this.isMessageFieldEmpty = new ReactiveVar(true); + this.showMicButton = new ReactiveVar(false); + this.showVideoRec = new ReactiveVar(false); + return this.autorun(() => { + const videoRegex = /video\/webm|video\/\*/i; + const videoEnabled = !RocketChat.settings.get('FileUpload_MediaTypeWhiteList') || RocketChat.settings.get('FileUpload_MediaTypeWhiteList').match(videoRegex); + if (RocketChat.settings.get('Message_VideoRecorderEnabled') && ((navigator.getUserMedia != null) || (navigator.webkitGetUserMedia != null)) && videoEnabled && RocketChat.settings.get('FileUpload_Enabled')) { + this.showVideoRec.set(true); + } else { + this.showVideoRec.set(false); + } + const wavRegex = /audio\/wav|audio\/\*/i; + const wavEnabled = !RocketChat.settings.get('FileUpload_MediaTypeWhiteList') || RocketChat.settings.get('FileUpload_MediaTypeWhiteList').match(wavRegex); + if (RocketChat.settings.get('Message_AudioRecorderEnabled') && ((navigator.getUserMedia != null) || (navigator.webkitGetUserMedia != null)) && wavEnabled && RocketChat.settings.get('FileUpload_Enabled')) { + return this.showMicButton.set(true); + } else { + return this.showMicButton.set(false); + } + }); +}); + +Meteor.startup(function() { + RocketChat.Geolocation = new ReactiveVar(false); + return Tracker.autorun(function() { + const MapView_GMapsAPIKey = RocketChat.settings.get('MapView_GMapsAPIKey'); + if (RocketChat.settings.get('MapView_Enabled') === true && MapView_GMapsAPIKey && MapView_GMapsAPIKey.length && navigator.geolocation && navigator.geolocation.getCurrentPosition) { + const success = (position) => { + return RocketChat.Geolocation.set(position); + }; + const error = (error) => { + console.log('Error getting your geolocation', error); + return RocketChat.Geolocation.set(false); + }; + const options = { + enableHighAccuracy: true, + maximumAge: 0, + timeout: 10000 + }; + return navigator.geolocation.watchPosition(success, error, options); + } else { + return RocketChat.Geolocation.set(false); + } + }); +}); diff --git a/packages/rocketchat-ui-message/client/popup/messagePopup.coffee b/packages/rocketchat-ui-message/client/popup/messagePopup.coffee deleted file mode 100644 index c76fcf1b1711fe501d76781c1c41d858518bbecd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/popup/messagePopup.coffee +++ /dev/null @@ -1,282 +0,0 @@ -# This is not supposed to be a complete list -# it is just to improve readability in this file -keys = { - TAB: 9 - ENTER: 13 - ESC: 27 - ARROW_LEFT: 37 - ARROW_UP: 38 - ARROW_RIGHT: 39 - ARROW_DOWN: 40 -} - -getCursorPosition = (input) -> - if not input? then return - if input.selectionStart? - return input.selectionStart - else if document.selection? - input.focus() - sel = document.selection.createRange() - selLen = document.selection.createRange().text.length - sel.moveStart('character', - input.value.length) - return sel.text.length - selLen - -setCursorPosition = (input, caretPos) -> - if not input? then return - if input.selectionStart? - input.focus() - return input.setSelectionRange(caretPos, caretPos) - else if document.selection? - range = input.createTextRange() - range.move('character', caretPos) - range.select() - -val = (v, d) -> - return if v? then v else d - -Template.messagePopup.onCreated -> - template = this - - template.textFilter = new ReactiveVar '' - - template.textFilterDelay = val(template.data.textFilterDelay, 0) - - template.open = val(template.data.open, new ReactiveVar(false)) - - template.hasData = new ReactiveVar false - - template.value = new ReactiveVar - - template.trigger = val(template.data.trigger, '') - - template.triggerAnywhere = val(template.data.triggerAnywhere, true) - - template.closeOnEsc = val(template.data.closeOnEsc, true) - - template.blurOnSelectItem = val(template.data.blurOnSelectItem, false) - - template.prefix = val(template.data.prefix, template.trigger) - - template.suffix = val(template.data.suffix, '') - - if template.triggerAnywhere is true - template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp "(?:^| )#{template.trigger}[^\\s]*$") - else - template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp "(?:^)#{template.trigger}[^\\s]*$") - - template.selectorRegex = val(template.data.selectorRegex, new RegExp "#{template.trigger}([^\\s]*)$") - - template.replaceRegex = val(template.data.replaceRegex, new RegExp "#{template.trigger}[^\\s]*$") - - template.getValue = val template.data.getValue, (_id) -> return _id - - template.up = => - current = template.find('.popup-item.selected') - previous = $(current).prev('.popup-item')[0] or template.find('.popup-item:last-child') - if previous? - current.className = current.className.replace /\sselected/, '' - previous.className += ' selected' - template.value.set previous.getAttribute('data-id') - - template.down = => - current = template.find('.popup-item.selected') - next = $(current).next('.popup-item')[0] or template.find('.popup-item') - if next?.classList.contains('popup-item') - current.className = current.className.replace /\sselected/, '' - next.className += ' selected' - template.value.set next.getAttribute('data-id') - - template.verifySelection = => - current = template.find('.popup-item.selected') - if not current? - first = template.find('.popup-item') - if first? - first.className += ' selected' - template.value.set first.getAttribute('data-id') - else - template.value.set undefined - - template.onInputKeydown = (event) => - if template.open.curValue isnt true or template.hasData.curValue isnt true - return - - if event.which in [keys.ENTER, keys.TAB] - if template.blurOnSelectItem is true - template.input.blur() - else - template.open.set false - - template.enterValue() - - if template.data.cleanOnEnter - template.input.value = '' - - event.preventDefault() - event.stopPropagation() - return - - if event.which is keys.ARROW_UP - template.up() - - event.preventDefault() - event.stopPropagation() - return - - if event.which is keys.ARROW_DOWN - template.down() - - event.preventDefault() - event.stopPropagation() - return - - template.setTextFilter = _.debounce (value) -> - template.textFilter.set(value) - , template.textFilterDelay - - template.onInputKeyup = (event) => - if template.closeOnEsc is true and template.open.curValue is true and event.which is keys.ESC - template.open.set false - event.preventDefault() - event.stopPropagation() - return - - value = template.input.value - value = value.substr 0, getCursorPosition(template.input) - - if template.matchSelectorRegex.test value - template.setTextFilter value.match(template.selectorRegex)[1] - template.open.set true - else - template.open.set false - - if template.open.curValue isnt true - return - - if event.which not in [keys.ARROW_UP, keys.ARROW_DOWN] - Meteor.defer => - template.verifySelection() - - template.onFocus = (event) => - template.clickingItem = false; - - if template.open.curValue is true - return - - value = template.input.value - value = value.substr 0, getCursorPosition(template.input) - - if template.matchSelectorRegex.test value - template.setTextFilter value.match(template.selectorRegex)[1] - template.open.set true - Meteor.defer => - template.verifySelection() - else - template.open.set false - - template.onBlur = (event) => - if template.open.curValue is false - return - - if template.clickingItem is true - return - - template.open.set false - - template.enterValue = -> - if not template.value.curValue? then return - - value = template.input.value - caret = getCursorPosition(template.input) - firstPartValue = value.substr 0, caret - lastPartValue = value.substr caret - getValue = this.getValue(template.value.curValue, template.data.collection, template.records.get(), firstPartValue) - - if not getValue - return - - firstPartValue = firstPartValue.replace(template.selectorRegex, template.prefix + getValue + template.suffix) - - template.input.value = firstPartValue + lastPartValue - - setCursorPosition template.input, firstPartValue.length - - template.records = new ReactiveVar [] - Tracker.autorun -> - if template.data.collection.findOne? - template.data.collection.find().count() - - filter = template.textFilter.get() - if filter? - filterCallback = (result) => - template.hasData.set result?.length > 0 - template.records.set result - - Meteor.defer => - template.verifySelection() - - result = template.data.getFilter(template.data.collection, filter, filterCallback) - if result? - filterCallback result - - -Template.messagePopup.onRendered -> - if this.data.getInput? - this.input = this.data.getInput?() - else if this.data.input - this.input = this.parentTemplate().find(this.data.input) - - if not this.input? - console.error 'Input not found for popup' - - $(this.input).on 'keyup', this.onInputKeyup.bind this - $(this.input).on 'keydown', this.onInputKeydown.bind this - $(this.input).on 'focus', this.onFocus.bind this - $(this.input).on 'blur', this.onBlur.bind this - - -Template.messagePopup.onDestroyed -> - $(this.input).off 'keyup', this.onInputKeyup - $(this.input).off 'keydown', this.onInputKeydown - $(this.input).off 'focus', this.onFocus - $(this.input).off 'blur', this.onBlur - - -Template.messagePopup.events - 'mouseenter .popup-item': (e) -> - if e.currentTarget.className.indexOf('selected') > -1 - return - - template = Template.instance() - - current = template.find('.popup-item.selected') - if current? - current.className = current.className.replace /\sselected/, '' - e.currentTarget.className += ' selected' - template.value.set this._id - - 'mousedown .popup-item, touchstart .popup-item': (e) -> - template = Template.instance() - template.clickingItem = true; - - 'mouseup .popup-item, touchend .popup-item': (e) -> - template = Template.instance() - - template.clickingItem = false; - - template.value.set this._id - - template.enterValue() - - template.open.set false - - toolbarSearch.clear(); - - -Template.messagePopup.helpers - isOpen: -> - Template.instance().open.get() and ((Template.instance().hasData.get() or Template.instance().data.emptyTemplate?) or not Template.instance().parentTemplate(1).subscriptionsReady()) - - data: -> - template = Template.instance() - - return template.records.get() diff --git a/packages/rocketchat-ui-message/client/popup/messagePopup.js b/packages/rocketchat-ui-message/client/popup/messagePopup.js new file mode 100644 index 0000000000000000000000000000000000000000..57a160bc13bb6e75e868ea6c4d4c20f2cf324732 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopup.js @@ -0,0 +1,285 @@ +/* globals toolbarSearch */ +// This is not supposed to be a complete list +// it is just to improve readability in this file +const keys = { + TAB: 9, + ENTER: 13, + ESC: 27, + ARROW_LEFT: 37, + ARROW_UP: 38, + ARROW_RIGHT: 39, + ARROW_DOWN: 40 +}; + +function getCursorPosition(input) { + if (input == null) { + return; + } + if (input.selectionStart != null) { + return input.selectionStart; + } else if (document.selection != null) { + input.focus(); + const sel = document.selection.createRange(); + const selLen = document.selection.createRange().text.length; + sel.moveStart('character', -input.value.length); + return sel.text.length - selLen; + } +} + +function setCursorPosition(input, caretPos) { + if (input == null) { + return; + } + if (input.selectionStart != null) { + input.focus(); + return input.setSelectionRange(caretPos, caretPos); + } else if (document.selection != null) { + const range = input.createTextRange(); + range.move('character', caretPos); + return range.select(); + } +} + +function val(v, d) { + if (v != null) { + return v; + } else { + return d; + } +} + +Template.messagePopup.onCreated(function() { + const template = this; + template.textFilter = new ReactiveVar(''); + template.textFilterDelay = val(template.data.textFilterDelay, 0); + template.open = val(template.data.open, new ReactiveVar(false)); + template.hasData = new ReactiveVar(false); + template.value = new ReactiveVar; + template.trigger = val(template.data.trigger, ''); + template.triggerAnywhere = val(template.data.triggerAnywhere, true); + template.closeOnEsc = val(template.data.closeOnEsc, true); + template.blurOnSelectItem = val(template.data.blurOnSelectItem, false); + template.prefix = val(template.data.prefix, template.trigger); + template.suffix = val(template.data.suffix, ''); + if (template.triggerAnywhere === true) { + template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp(`(?:^| )${ template.trigger }[^\\s]*$`)); + } else { + template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp(`(?:^)${ template.trigger }[^\\s]*$`)); + } + template.selectorRegex = val(template.data.selectorRegex, new RegExp(`${ template.trigger }([^\\s]*)$`)); + template.replaceRegex = val(template.data.replaceRegex, new RegExp(`${ template.trigger }[^\\s]*$`)); + template.getValue = val(template.data.getValue, function(_id) { + return _id; + }); + template.up = () => { + const current = template.find('.popup-item.selected'); + const previous = $(current).prev('.popup-item')[0] || template.find('.popup-item:last-child'); + if (previous != null) { + current.className = current.className.replace(/\sselected/, ''); + previous.className += ' selected'; + return template.value.set(previous.getAttribute('data-id')); + } + }; + template.down = () => { + const current = template.find('.popup-item.selected'); + const next = $(current).next('.popup-item')[0] || template.find('.popup-item'); + if (next && next.classList.contains('popup-item')) { + current.className = current.className.replace(/\sselected/, ''); + next.className += ' selected'; + return template.value.set(next.getAttribute('data-id')); + } + }; + template.verifySelection = () => { + const current = template.find('.popup-item.selected'); + if (current == null) { + const first = template.find('.popup-item'); + if (first != null) { + first.className += ' selected'; + return template.value.set(first.getAttribute('data-id')); + } else { + return template.value.set(null); + } + } + }; + template.onInputKeydown = (event) => { + if (template.open.curValue !== true || template.hasData.curValue !== true) { + return; + } + if (event.which === keys.ENTER || event.which === keys.TAB) { + if (template.blurOnSelectItem === true) { + template.input.blur(); + } else { + template.open.set(false); + } + template.enterValue(); + if (template.data.cleanOnEnter) { + template.input.value = ''; + } + event.preventDefault(); + event.stopPropagation(); + return; + } + if (event.which === keys.ARROW_UP) { + template.up(); + event.preventDefault(); + event.stopPropagation(); + return; + } + if (event.which === keys.ARROW_DOWN) { + template.down(); + event.preventDefault(); + event.stopPropagation(); + } + }; + + template.setTextFilter = _.debounce(function(value) { + return template.textFilter.set(value); + }, template.textFilterDelay); + + template.onInputKeyup = (event) => { + if (template.closeOnEsc === true && template.open.curValue === true && event.which === keys.ESC) { + template.open.set(false); + event.preventDefault(); + event.stopPropagation(); + return; + } + const value = template.input.value.substr(0, getCursorPosition(template.input)); + + if (template.matchSelectorRegex.test(value)) { + template.setTextFilter(value.match(template.selectorRegex)[1]); + template.open.set(true); + } else { + template.open.set(false); + } + if (template.open.curValue !== true) { + return; + } + if (event.which !== keys.ARROW_UP && event.which !== keys.ARROW_DOWN) { + return Meteor.defer(function() { + template.verifySelection(); + }); + } + }; + template.onFocus = () => { + template.clickingItem = false; + if (template.open.curValue === true) { + return; + } + const value = template.input.value.substr(0, getCursorPosition(template.input)); + if (template.matchSelectorRegex.test(value)) { + template.setTextFilter(value.match(template.selectorRegex)[1]); + template.open.set(true); + return Meteor.defer(function() { + return template.verifySelection(); + }); + } else { + return template.open.set(false); + } + }; + + template.onBlur = () => { + if (template.open.curValue === false) { + return; + } + if (template.clickingItem === true) { + return; + } + return template.open.set(false); + }; + + template.enterValue = function() { + if (template.value.curValue == null) { + return; + } + const value = template.input.value; + const caret = getCursorPosition(template.input); + let firstPartValue = value.substr(0, caret); + const lastPartValue = value.substr(caret); + const getValue = this.getValue(template.value.curValue, template.data.collection, template.records.get(), firstPartValue); + if (!getValue) { + return; + } + firstPartValue = firstPartValue.replace(template.selectorRegex, template.prefix + getValue + template.suffix); + template.input.value = firstPartValue + lastPartValue; + return setCursorPosition(template.input, firstPartValue.length); + }; + template.records = new ReactiveVar([]); + Tracker.autorun(function() { + if (template.data.collection.findOne != null) { + template.data.collection.find().count(); + } + const filter = template.textFilter.get(); + if (filter != null) { + const filterCallback = (result) => { + template.hasData.set(result && result.length > 0); + template.records.set(result); + return Meteor.defer(function() { + return template.verifySelection(); + }); + }; + const result = template.data.getFilter(template.data.collection, filter, filterCallback); + if (result != null) { + return filterCallback(result); + } + } + }); +}); + +Template.messagePopup.onRendered(function() { + if (this.data.getInput != null) { + this.input = typeof this.data.getInput === 'function' && this.data.getInput(); + } else if (this.data.input) { + this.input = this.parentTemplate().find(this.data.input); + } + if (this.input == null) { + console.error('Input not found for popup'); + } + $(this.input).on('keyup', this.onInputKeyup.bind(this)); + $(this.input).on('keydown', this.onInputKeydown.bind(this)); + $(this.input).on('focus', this.onFocus.bind(this)); + return $(this.input).on('blur', this.onBlur.bind(this)); +}); + +Template.messagePopup.onDestroyed(function() { + $(this.input).off('keyup', this.onInputKeyup); + $(this.input).off('keydown', this.onInputKeydown); + $(this.input).off('focus', this.onFocus); + return $(this.input).off('blur', this.onBlur); +}); + +Template.messagePopup.events({ + 'mouseenter .popup-item'(e) { + if (e.currentTarget.className.indexOf('selected') > -1) { + return; + } + const template = Template.instance(); + const current = template.find('.popup-item.selected'); + if (current != null) { + current.className = current.className.replace(/\sselected/, ''); + } + e.currentTarget.className += ' selected'; + return template.value.set(this._id); + }, + 'mousedown .popup-item, touchstart .popup-item'() { + const template = Template.instance(); + return template.clickingItem = true; + }, + 'mouseup .popup-item, touchend .popup-item'() { + const template = Template.instance(); + template.clickingItem = false; + template.value.set(this._id); + template.enterValue(); + template.open.set(false); + return toolbarSearch.clear(); + } +}); + +Template.messagePopup.helpers({ + isOpen() { + return Template.instance().open.get() && ((Template.instance().hasData.get() || (Template.instance().data.emptyTemplate != null)) || !Template.instance().parentTemplate(1).subscriptionsReady()); + }, + data() { + const template = Template.instance(); + return template.records.get(); + } +}); diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee deleted file mode 100644 index a5bc17cc33fc8ed2fd76ebf284b1ac7c42028ec2..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee +++ /dev/null @@ -1,235 +0,0 @@ -@filteredUsersMemory = new Mongo.Collection null - -Meteor.startup -> - Tracker.autorun -> - if not Meteor.user()? or not Session.get('openedRoom')? - return - - filteredUsersMemory.remove({}) - messageUsers = RocketChat.models.Messages.find({rid: Session.get('openedRoom'), 'u.username': {$ne: Meteor.user().username}}, {fields: {'u.username': 1, 'u.name': 1, ts: 1}, sort: {ts: -1}}).fetch() - uniqueMessageUsersControl = {} - messageUsers.forEach (messageUser) -> - if not uniqueMessageUsersControl[messageUser.u.username]? - uniqueMessageUsersControl[messageUser.u.username] = true - filteredUsersMemory.upsert messageUser.u.username, - _id: messageUser.u.username - username: messageUser.u.username - name: messageUser.u.name - status: Session.get('user_' + messageUser.u.username + '_status') or 'offline' - ts: messageUser.ts - - -getUsersFromServer = (filter, records, cb) => - messageUsers = _.pluck(records, 'username') - Meteor.call 'spotlight', filter, messageUsers, { users: true }, (err, results) -> - if err? - return console.error err - - if results.users.length > 0 - for result in results.users - if records.length < 5 - records.push - _id: result.username - username: result.username - status: 'offline' - sort: 3 - - records = _.sortBy(records, 'sort') - - cb(records) - -getRoomsFromServer = (filter, records, cb) => - Meteor.call 'spotlight', filter, null, { rooms: true }, (err, results) -> - if err? - return console.error err - - if results.rooms.length > 0 - for room in results.rooms - if records.length < 5 - records.push room - - cb(records) - -getUsersFromServerDelayed = _.throttle getUsersFromServer, 500 -getRoomsFromServerDelayed = _.throttle getRoomsFromServer, 500 - - -Template.messagePopupConfig.helpers - popupUserConfig: -> - self = this - template = Template.instance() - - config = - title: t('People') - collection: filteredUsersMemory - template: 'messagePopupUser' - getInput: self.getInput - textFilterDelay: 200 - trigger: '@' - suffix: ' ' - getFilter: (collection, filter, cb) -> - exp = new RegExp("#{RegExp.escape filter}", 'i') - - # Get users from messages - items = filteredUsersMemory.find({ts: {$exists: true}, $or: [{username: exp}, {name: exp}]}, {limit: 5, sort: {ts: -1}}).fetch() - - # Get online users - if items.length < 5 and filter?.trim() isnt '' - messageUsers = _.pluck(items, 'username') - Meteor.users.find({$and: [{$or:[{username: exp}, {name: exp}]}, {username: {$nin: [Meteor.user()?.username].concat(messageUsers)}}]}, {limit: 5 - messageUsers.length}).fetch().forEach (item) -> - items.push - _id: item.username - username: item.username - name: item.name - status: item.status - sort: 1 - - # # Get users of room - # if items.length < 5 and filter?.trim() isnt '' - # messageUsers = _.pluck(items, 'username') - # Tracker.nonreactive -> - # roomUsernames = RocketChat.models.Rooms.findOne(Session.get('openedRoom')).usernames - # for roomUsername in roomUsernames - # if messageUsers.indexOf(roomUsername) is -1 and exp.test(roomUsername) - # items.push - # _id: roomUsername - # username: roomUsername - # status: Session.get('user_' + roomUsername + '_status') or 'offline' - # sort: 2 - - # if items.length >= 5 - # break - - # Get users from db - if items.length < 5 and filter?.trim() isnt '' - getUsersFromServerDelayed filter, items, cb - - all = - _id: 'all' - username: 'all' - system: true - name: t 'Notify_all_in_this_room' - compatibility: 'channel group' - sort: 4 - - exp = new RegExp("(^|\\s)#{RegExp.escape filter}", 'i') - if exp.test(all.username) or exp.test(all.compatibility) - items.push all - - here = - _id: 'here' - username: 'here' - system: true - name: t 'Notify_active_in_this_room' - compatibility: 'channel group' - sort: 4 - - if exp.test(here.username) or exp.test(here.compatibility) - items.push here - - return items - - getValue: (_id) -> - return _id - - return config - - popupChannelConfig: -> - self = this - template = Template.instance() - - config = - title: t('Channels') - collection: RocketChat.models.Subscriptions - trigger: '#' - suffix: ' ' - template: 'messagePopupChannel' - getInput: self.getInput - getFilter: (collection, filter, cb) -> - exp = new RegExp(filter, 'i') - - records = collection.find({name: exp, t: {$in: ['c', 'p']}}, {limit: 5, sort: {ls: -1}}).fetch() - - if records.length < 5 and filter?.trim() isnt '' - getRoomsFromServerDelayed filter, records, cb - - return records - - getValue: (_id, collection, records) -> - return _.findWhere(records, {_id: _id})?.name - - return config - - popupSlashCommandsConfig: -> - self = this - template = Template.instance() - - config = - title: t('Commands') - collection: RocketChat.slashCommands.commands - trigger: '/' - suffix: ' ' - triggerAnywhere: false - template: 'messagePopupSlashCommand' - getInput: self.getInput - getFilter: (collection, filter) -> - commands = [] - for command, item of collection - if command.indexOf(filter) > -1 - commands.push - _id: command - params: if item.params then TAPi18n.__ item.params else '' - description: TAPi18n.__ item.description - - commands = commands.sort (a, b) -> - return a._id > b._id - - commands = commands[0..10] - - return commands - - return config - - emojiEnabled: -> - return RocketChat.emoji? - - popupEmojiConfig: -> - if RocketChat.emoji? - self = this - template = Template.instance() - config = - title: t('Emoji') - collection: RocketChat.emoji.list - template: 'messagePopupEmoji' - trigger: ':' - prefix: '' - suffix: ' ' - getInput: self.getInput - getFilter: (collection, filter, cb) -> - results = [] - key = ':' + filter - - if RocketChat.emoji.packages.emojione?.asciiList[key] or filter.length < 2 - return [] - - regExp = new RegExp('^' + RegExp.escape(key), 'i') - - for key, value of collection - if results.length > 10 - break - - if regExp.test(key) - results.push - _id: key - data: value - - results.sort (a, b) -> - if a._id < b._id - return -1 - if a._id > b._id - return 1 - return 0 - - return results - - return config diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js new file mode 100644 index 0000000000000000000000000000000000000000..797e77fa9ca4dbdd6c993f1228f88d0e96b2f9b4 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js @@ -0,0 +1,295 @@ +/* globals filteredUsersMemory */ +filteredUsersMemory = new Mongo.Collection(null); + +Meteor.startup(function() { + Tracker.autorun(function() { + if (Meteor.user() == null || Session.get('openedRoom') == null) { + return; + } + filteredUsersMemory.remove({}); + const messageUsers = RocketChat.models.Messages.find({ + rid: Session.get('openedRoom'), + 'u.username': { + $ne: Meteor.user().username + } + }, { + fields: { + 'u.username': 1, + 'u.name': 1, + ts: 1 + }, + sort: { + ts: -1 + } + }).fetch(); + const uniqueMessageUsersControl = {}; + return messageUsers.forEach(function(messageUser) { + if (uniqueMessageUsersControl[messageUser.u.username] == null) { + uniqueMessageUsersControl[messageUser.u.username] = true; + return filteredUsersMemory.upsert(messageUser.u.username, { + _id: messageUser.u.username, + username: messageUser.u.username, + name: messageUser.u.name, + status: Session.get(`user_${ messageUser.u.username }_status`) || 'offline', + ts: messageUser.ts + }); + } + }); + }); +}); + +const getUsersFromServer = (filter, records, cb) => { + const messageUsers = _.pluck(records, 'username'); + return Meteor.call('spotlight', filter, messageUsers, { + users: true + }, function(err, results) { + if (err != null) { + return console.error(err); + } + if (results.users.length > 0) { + results.users.forEach(result => { + if (records.length < 5) { + records.push({ + _id: result.username, + username: result.username, + status: 'offline', + sort: 3 + }); + } + }); + records = _.sortBy(records, 'sort'); + return cb(records); + } + }); +}; + +const getRoomsFromServer = (filter, records, cb) => { + return Meteor.call('spotlight', filter, null, { + rooms: true + }, function(err, results) { + if (err != null) { + return console.error(err); + } + if (results.rooms.length > 0) { + results.rooms.forEach(room => { + if (records.length < 5) { + records.push(room); + } + }); + return cb(records); + } + }); +}; + +const getUsersFromServerDelayed = _.throttle(getUsersFromServer, 500); + +const getRoomsFromServerDelayed = _.throttle(getRoomsFromServer, 500); + +Template.messagePopupConfig.helpers({ + popupUserConfig() { + const self = this; + const config = { + title: t('People'), + collection: filteredUsersMemory, + template: 'messagePopupUser', + getInput: self.getInput, + textFilterDelay: 200, + trigger: '@', + suffix: ' ', + getFilter(collection, filter, cb) { + let exp = new RegExp(`${ RegExp.escape(filter) }`, 'i'); + // Get users from messages + const items = filteredUsersMemory.find({ + ts: { + $exists: true + }, + $or: [ + { + username: exp + }, { + name: exp + } + ] + }, { + limit: 5, + sort: { + ts: -1 + } + }).fetch(); + // Get online users + if (items.length < 5 && filter && filter.trim() !== '') { + const messageUsers = _.pluck(items, 'username'); + const user = Meteor.user(); + items.push(...Meteor.users.find({ + $and: [ + { + $or: [ + { + username: exp + }, { + name: exp + } + ] + }, { + username: { + $nin: [(user && user.username), ...messageUsers] + } + } + ] + }, { + limit: 5 - messageUsers.length + }).fetch().map(function(item) { + return { + _id: item.username, + username: item.username, + name: item.name, + status: item.status, + sort: 1 + }; + })); + } + // Get users from db + if (items.length < 5 && filter && filter.trim() !== '') { + getUsersFromServerDelayed(filter, items, cb); + } + const all = { + _id: 'all', + username: 'all', + system: true, + name: t('Notify_all_in_this_room'), + compatibility: 'channel group', + sort: 4 + }; + exp = new RegExp(`(^|\\s)${ RegExp.escape(filter) }`, 'i'); + if (exp.test(all.username) || exp.test(all.compatibility)) { + items.push(all); + } + const here = { + _id: 'here', + username: 'here', + system: true, + name: t('Notify_active_in_this_room'), + compatibility: 'channel group', + sort: 4 + }; + if (exp.test(here.username) || exp.test(here.compatibility)) { + items.push(here); + } + return items; + }, + getValue(_id) { + return _id; + } + }; + return config; + }, + popupChannelConfig() { + const self = this; + const config = { + title: t('Channels'), + collection: RocketChat.models.Subscriptions, + trigger: '#', + suffix: ' ', + template: 'messagePopupChannel', + getInput: self.getInput, + getFilter(collection, filter, cb) { + const exp = new RegExp(filter, 'i'); + const records = collection.find({ + name: exp, + t: { + $in: ['c', 'p'] + } + }, { + limit: 5, + sort: { + ls: -1 + } + }).fetch(); + + if (records.length < 5 && filter && filter.trim() !== '') { + getRoomsFromServerDelayed(filter, records, cb); + } + return records; + }, + getValue(_id, collection, records) { + const record = _.findWhere(records, { + _id + }); + return record && record.name; + } + }; + return config; + }, + popupSlashCommandsConfig() { + const self = this; + const config = { + title: t('Commands'), + collection: RocketChat.slashCommands.commands, + trigger: '/', + suffix: ' ', + triggerAnywhere: false, + template: 'messagePopupSlashCommand', + getInput: self.getInput, + getFilter(collection, filter) { + return Object.keys(collection).map(command => { + const item = collection[command]; + return { + _id: command, + params: item.params ? TAPi18n.__(item.params) : '', + description: TAPi18n.__(item.description) + }; + }) + .filter(command => command._id.indexOf(filter) > -1) + .sort(function(a, b) { + return a._id > b._id; + }) + .slice(0, 11); + } + }; + return config; + }, + emojiEnabled() { + return RocketChat.emoji != null; + }, + popupEmojiConfig() { + 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]; + 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; + }); + } + }; + } + } +}); diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee deleted file mode 100644 index d50a31f2c236aa0a71d4bc5144f84d2101e4a68d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee +++ /dev/null @@ -1,4 +0,0 @@ -Template.messagePopupEmoji.helpers - value: -> - length = this.data.length - return this.data[length - 1] diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js new file mode 100644 index 0000000000000000000000000000000000000000..934067a235e72a70cbf29af8cd85cb262cd640c8 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js @@ -0,0 +1,6 @@ +Template.messagePopupEmoji.helpers({ + value() { + const length = this.data.length; + return this.data[length - 1]; + } +}); diff --git a/packages/rocketchat-ui-message/package.js b/packages/rocketchat-ui-message/package.js index 4d26d912832fbb5d6ff45c6ab902d02cabf39663..bcf2b9e3ddfd66b221c62bd5091f7ecab70fdcd1 100644 --- a/packages/rocketchat-ui-message/package.js +++ b/packages/rocketchat-ui-message/package.js @@ -15,7 +15,6 @@ Package.onUse(function(api) { 'mongo', 'ecmascript', 'templating', - 'coffeescript', 'underscore', 'tracker', 'rocketchat:lib', @@ -33,12 +32,12 @@ Package.onUse(function(api) { api.addFiles('client/popup/messagePopupSlashCommand.html', 'client'); api.addFiles('client/popup/messagePopupUser.html', 'client'); - api.addFiles('client/message.coffee', 'client'); - api.addFiles('client/messageBox.coffee', 'client'); - api.addFiles('client/popup/messagePopup.coffee', 'client'); + api.addFiles('client/message.js', 'client'); + api.addFiles('client/messageBox.js', 'client'); + api.addFiles('client/popup/messagePopup.js', 'client'); api.addFiles('client/popup/messagePopupChannel.js', 'client'); - api.addFiles('client/popup/messagePopupConfig.coffee', 'client'); - api.addFiles('client/popup/messagePopupEmoji.coffee', 'client'); + api.addFiles('client/popup/messagePopupConfig.js', 'client'); + api.addFiles('client/popup/messagePopupEmoji.js', 'client'); api.addFiles('client/renderMessageBody.js', 'client'); diff --git a/packages/rocketchat-ui/client/lib/fileUpload.coffee b/packages/rocketchat-ui/client/lib/fileUpload.coffee index 55507fe06189668c34d6907074167a26b5f58fe6..e4494b7d78683b94332b90b20b9f7b2505f282f0 100644 --- a/packages/rocketchat-ui/client/lib/fileUpload.coffee +++ b/packages/rocketchat-ui/client/lib/fileUpload.coffee @@ -9,6 +9,8 @@ getUploadPreview = (file, callback) -> # If greater then 10MB don't try and show a preview if file.file.size > 10 * 1000000 callback(file, null) + else if not file.file.type? + callback(file, null) else if file.file.type.indexOf('audio') > -1 or file.file.type.indexOf('video') > -1 or file.file.type.indexOf('image') > -1 file.type = file.file.type.split('/')[0] diff --git a/packages/rocketchat-ui/client/lib/fireEvent.js b/packages/rocketchat-ui/client/lib/fireEvent.js index 3a1ae3fe6c102a280980407250817f967bd7c2fa..a3456d3e61a6d235a9b1d180b6c1be4c54ea2718 100644 --- a/packages/rocketchat-ui/client/lib/fireEvent.js +++ b/packages/rocketchat-ui/client/lib/fireEvent.js @@ -1,4 +1,6 @@ window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { + window.dispatchEvent(new CustomEvent(eventName, {detail: params})); + Tracker.autorun((computation) => { const enabled = RocketChat.settings.get('Iframe_Integration_send_enable'); if (enabled === undefined) { @@ -6,7 +8,6 @@ window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { } computation.stop(); if (enabled) { - window.dispatchEvent(new CustomEvent(eventName, {detail: params})); parent.postMessage({ eventName, data: params @@ -14,3 +15,4 @@ window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { } }); }; + diff --git a/packages/rocketchat-webrtc/WebRTCClass.coffee b/packages/rocketchat-webrtc/WebRTCClass.coffee deleted file mode 100644 index a957e411d7cc480832d41bd9cdc3ce66a4715c50..0000000000000000000000000000000000000000 --- a/packages/rocketchat-webrtc/WebRTCClass.coffee +++ /dev/null @@ -1,823 +0,0 @@ -emptyFn = -> - # empty - -class WebRTCTransportClass - debug: false - - log: -> - if @debug is true - console.log.apply(console, arguments) - - constructor: (@webrtcInstance) -> - @callbacks = {} - - RocketChat.Notifications.onRoom @webrtcInstance.room, 'webrtc', (type, data) => - @log 'WebRTCTransportClass - onRoom', type, data - - switch type - when 'status' - if @callbacks['onRemoteStatus']?.length > 0 - fn(data) for fn in @callbacks['onRemoteStatus'] - - onUserStream: (type, data) -> - if data.room isnt @webrtcInstance.room then return - @log 'WebRTCTransportClass - onUser', type, data - - switch type - when 'call' - if @callbacks['onRemoteCall']?.length > 0 - fn(data) for fn in @callbacks['onRemoteCall'] - - when 'join' - if @callbacks['onRemoteJoin']?.length > 0 - fn(data) for fn in @callbacks['onRemoteJoin'] - - when 'candidate' - if @callbacks['onRemoteCandidate']?.length > 0 - fn(data) for fn in @callbacks['onRemoteCandidate'] - - when 'description' - if @callbacks['onRemoteDescription']?.length > 0 - fn(data) for fn in @callbacks['onRemoteDescription'] - - startCall: (data) -> - @log 'WebRTCTransportClass - startCall', @webrtcInstance.room, @webrtcInstance.selfId - RocketChat.Notifications.notifyUsersOfRoom @webrtcInstance.room, 'webrtc', 'call', - from: @webrtcInstance.selfId - room: @webrtcInstance.room - media: data.media - monitor: data.monitor - - joinCall: (data) -> - @log 'WebRTCTransportClass - joinCall', @webrtcInstance.room, @webrtcInstance.selfId - if data.monitor is true - RocketChat.Notifications.notifyUser data.to, 'webrtc', 'join', - from: @webrtcInstance.selfId - room: @webrtcInstance.room - media: data.media - monitor: data.monitor - else - RocketChat.Notifications.notifyUsersOfRoom @webrtcInstance.room, 'webrtc', 'join', - from: @webrtcInstance.selfId - room: @webrtcInstance.room - media: data.media - monitor: data.monitor - - sendCandidate: (data) -> - data.from = @webrtcInstance.selfId - data.room = @webrtcInstance.room - @log 'WebRTCTransportClass - sendCandidate', data - RocketChat.Notifications.notifyUser data.to, 'webrtc', 'candidate', data - - sendDescription: (data) -> - data.from = @webrtcInstance.selfId - data.room = @webrtcInstance.room - @log 'WebRTCTransportClass - sendDescription', data - RocketChat.Notifications.notifyUser data.to, 'webrtc', 'description', data - - sendStatus: (data) -> - @log 'WebRTCTransportClass - sendStatus', data, @webrtcInstance.room - data.from = @webrtcInstance.selfId - RocketChat.Notifications.notifyRoom @webrtcInstance.room, 'webrtc', 'status', data - - onRemoteCall: (fn) -> - @callbacks['onRemoteCall'] ?= [] - @callbacks['onRemoteCall'].push fn - - onRemoteJoin: (fn) -> - @callbacks['onRemoteJoin'] ?= [] - @callbacks['onRemoteJoin'].push fn - - onRemoteCandidate: (fn) -> - @callbacks['onRemoteCandidate'] ?= [] - @callbacks['onRemoteCandidate'].push fn - - onRemoteDescription: (fn) -> - @callbacks['onRemoteDescription'] ?= [] - @callbacks['onRemoteDescription'].push fn - - onRemoteStatus: (fn) -> - @callbacks['onRemoteStatus'] ?= [] - @callbacks['onRemoteStatus'].push fn - - -class WebRTCClass - config: - iceServers: [] - - debug: false - - transportClass: WebRTCTransportClass - - - ### - @param seldId {String} - @param room {String} - ### - constructor: (@selfId, @room) -> - @config.iceServers = [] - - servers = RocketChat.settings.get("WebRTC_Servers") - if servers?.trim() isnt '' - servers = servers.replace /\s/g, '' - servers = servers.split ',' - for server in servers - server = server.split '@' - serverConfig = - urls: server.pop() - - if server.length is 1 - server = server[0].split ':' - serverConfig.username = decodeURIComponent(server[0]) - serverConfig.credential = decodeURIComponent(server[1]) - - @config.iceServers.push serverConfig - - @peerConnections = {} - - @remoteItems = new ReactiveVar [] - @remoteItemsById = new ReactiveVar {} - @callInProgress = new ReactiveVar false - @audioEnabled = new ReactiveVar true - @videoEnabled = new ReactiveVar true - @overlayEnabled = new ReactiveVar false - @screenShareEnabled = new ReactiveVar false - @localUrl = new ReactiveVar - - @active = false - @remoteMonitoring = false - @monitor = false - @autoAccept = false - - @navigator = undefined - userAgent = navigator.userAgent.toLocaleLowerCase(); - if userAgent.indexOf('electron') isnt -1 - @navigator = 'electron' - else if userAgent.indexOf('chrome') isnt -1 - @navigator = 'chrome' - else if userAgent.indexOf('firefox') isnt -1 - @navigator = 'firefox' - else if userAgent.indexOf('safari') isnt -1 - @navigator = 'safari' - - @screenShareAvailable = @navigator in ['chrome', 'firefox', 'electron'] - - @media = - video: false - audio: true - - @transport = new @transportClass @ - - @transport.onRemoteCall @onRemoteCall.bind @ - @transport.onRemoteJoin @onRemoteJoin.bind @ - @transport.onRemoteCandidate @onRemoteCandidate.bind @ - @transport.onRemoteDescription @onRemoteDescription.bind @ - @transport.onRemoteStatus @onRemoteStatus.bind @ - - Meteor.setInterval @checkPeerConnections.bind(@), 1000 - - # Meteor.setInterval @broadcastStatus.bind(@), 1000 - - log: -> - if @debug is true - console.log.apply(console, arguments) - - onError: -> - console.error.apply(console, arguments) - - checkPeerConnections: -> - for id, peerConnection of @peerConnections - if peerConnection.iceConnectionState not in ['connected', 'completed'] and peerConnection.createdAt + 5000 < Date.now() - @stopPeerConnection id - - updateRemoteItems: -> - items = [] - itemsById = {} - - for id, peerConnection of @peerConnections - for remoteStream in peerConnection.getRemoteStreams() - item = - id: id - url: URL.createObjectURL(remoteStream) - state: peerConnection.iceConnectionState - - switch peerConnection.iceConnectionState - when 'checking' - item.stateText = 'Connecting...' - - when 'connected', 'completed' - item.stateText = 'Connected' - item.connected = true - - when 'disconnected' - item.stateText = 'Disconnected' - - when 'failed' - item.stateText = 'Failed' - - when 'closed' - item.stateText = 'Closed' - - items.push item - itemsById[id] = item - - @remoteItems.set items - @remoteItemsById.set itemsById - - resetCallInProgress: -> - @callInProgress.set false - - broadcastStatus: -> - if @active isnt true or @monitor is true or @remoteMonitoring is true then return - - remoteConnections = [] - for id, peerConnection of @peerConnections - remoteConnections.push - id: id - media: peerConnection.remoteMedia - - @transport.sendStatus - media: @media - remoteConnections: remoteConnections - - ### - @param data {Object} - from {String} - media {Object} - remoteConnections {Array[Object]} - id {String} - media {Object} - ### - onRemoteStatus: (data) -> - # @log 'onRemoteStatus', arguments - - @callInProgress.set true - - Meteor.clearTimeout @callInProgressTimeout - @callInProgressTimeout = Meteor.setTimeout @resetCallInProgress.bind(@), 2000 - - if @active isnt true then return - - remoteConnections = [{id: data.from, media: data.media}].concat data.remoteConnections - - for remoteConnection in remoteConnections - if remoteConnection.id isnt @selfId and not @peerConnections[remoteConnection.id]? - @log 'reconnecting with', remoteConnection.id - @onRemoteJoin - from: remoteConnection.id - media: remoteConnection.media - - ### - @param id {String} - ### - getPeerConnection: (id) -> - return @peerConnections[id] if @peerConnections[id]? - - peerConnection = new RTCPeerConnection @config - - peerConnection.createdAt = Date.now() - peerConnection.remoteMedia = {} - - @peerConnections[id] = peerConnection - - eventNames = [ - 'icecandidate' - 'addstream' - 'removestream' - 'iceconnectionstatechange' - 'datachannel' - 'identityresult' - 'idpassertionerror' - 'idpvalidationerror' - 'negotiationneeded' - 'peeridentity' - 'signalingstatechange' - ] - - for eventName in eventNames - peerConnection.addEventListener eventName, (e) => - @log id, e.type, e - - peerConnection.addEventListener 'icecandidate', (e) => - if not e.candidate? - return - - @transport.sendCandidate - to: id - candidate: - candidate: e.candidate.candidate - sdpMLineIndex: e.candidate.sdpMLineIndex - sdpMid: e.candidate.sdpMid - - peerConnection.addEventListener 'addstream', (e) => - @updateRemoteItems() - - peerConnection.addEventListener 'removestream', (e) => - @updateRemoteItems() - - peerConnection.addEventListener 'iceconnectionstatechange', (e) => - if peerConnection.iceConnectionState in ['disconnected', 'closed'] and peerConnection is @peerConnections[id] - @stopPeerConnection id - Meteor.setTimeout => - if Object.keys(@peerConnections).length is 0 - @stop() - , 3000 - - @updateRemoteItems() - - return peerConnection - - _getUserMedia: (media, onSuccess, onError) -> - onSuccessLocal = (stream) -> - if AudioContext? and stream.getAudioTracks().length > 0 - audioContext = new AudioContext - source = audioContext.createMediaStreamSource(stream) - - volume = audioContext.createGain() - source.connect(volume) - peer = audioContext.createMediaStreamDestination() - volume.connect(peer) - volume.gain.value = 0.6 - - stream.removeTrack(stream.getAudioTracks()[0]) - stream.addTrack(peer.stream.getAudioTracks()[0]) - stream.volume = volume - - this.audioContext = audioContext - - onSuccess(stream) - - navigator.getUserMedia media, onSuccessLocal, onError - - - getUserMedia: (media, onSuccess, onError=@onError) -> - if media.desktop isnt true - @_getUserMedia media, onSuccess, onError - return - - if @screenShareAvailable isnt true - console.log 'Screen share is not avaliable' - return - - getScreen = (audioStream) => - if document.cookie.indexOf("rocketchatscreenshare=chrome") is -1 and not window.rocketchatscreenshare? and @navigator isnt 'electron' - refresh = -> - swal - type: "warning" - title: TAPi18n.__ "Refresh_your_page_after_install_to_enable_screen_sharing" - - swal - type: "warning" - title: TAPi18n.__ "Screen_Share" - text: TAPi18n.__ "You_need_install_an_extension_to_allow_screen_sharing" - html: true - showCancelButton: true - confirmButtonText: TAPi18n.__ "Install_Extension" - cancelButtonText: TAPi18n.__ "Cancel" - , (isConfirm) => - if isConfirm - if @navigator is 'chrome' - url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf' - try - chrome.webstore.install url, refresh, -> - window.open(url) - refresh() - catch e - window.open(url) - refresh() - else if @navigator is 'firefox' - window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/') - refresh() - - return onError(false) - - getScreenSuccess = (stream) => - if audioStream? - stream.addTrack(audioStream.getAudioTracks()[0]) - onSuccess(stream) - - if @navigator is 'firefox' - media = - audio: media.audio - video: - mozMediaSource: 'window' - mediaSource: 'window' - @_getUserMedia media, getScreenSuccess, onError - else - ChromeScreenShare.getSourceId @navigator, (id) => - media = - audio: false - video: - mandatory: - chromeMediaSource: 'desktop' - chromeMediaSourceId: id - maxWidth: 1280 - maxHeight: 720 - - @_getUserMedia media, getScreenSuccess, onError - - if @navigator is 'firefox' or not media.audio? or media.audio is false - getScreen() - else - getAudioSuccess = (audioStream) => - getScreen(audioStream) - - getAudioError = => - getScreen() - - @_getUserMedia {audio: media.audio}, getAudioSuccess, getAudioError - - - ### - @param callback {Function} - ### - getLocalUserMedia: (callback) -> - @log 'getLocalUserMedia', arguments - - if @localStream? - return callback null, @localStream - - onSuccess = (stream) => - @localStream = stream - @localUrl.set URL.createObjectURL(stream) - - @videoEnabled.set @media.video is true - @audioEnabled.set @media.audio is true - - for id, peerConnection of @peerConnections - peerConnection.addStream stream - - callback null, @localStream - - onError = (error) => - callback false - @onError error - - @getUserMedia @media, onSuccess, onError - - - ### - @param id {String} - ### - stopPeerConnection: (id) -> - peerConnection = @peerConnections[id] - if not peerConnection? then return - - delete @peerConnections[id] - peerConnection.close() - - @updateRemoteItems() - - stopAllPeerConnections: -> - for id, peerConnection of @peerConnections - @stopPeerConnection id - window.audioContext?.close() - - setAudioEnabled: (enabled=true) -> - if @localStream? - if enabled is true and @media.audio isnt true - delete @localStream - @media.audio = true - @getLocalUserMedia => - @stopAllPeerConnections() - @joinCall() - else - @localStream.getAudioTracks().forEach (audio) -> audio.enabled = enabled - @audioEnabled.set enabled - - disableAudio: -> - @setAudioEnabled false - - enableAudio: -> - @setAudioEnabled true - - setVideoEnabled: (enabled=true) -> - if @localStream? - if enabled is true and @media.video isnt true - delete @localStream - @media.video = true - @getLocalUserMedia => - @stopAllPeerConnections() - @joinCall() - else - @localStream.getVideoTracks().forEach (video) -> video.enabled = enabled - @videoEnabled.set enabled - - disableScreenShare: -> - @setScreenShareEnabled false - - enableScreenShare: -> - @setScreenShareEnabled true - - setScreenShareEnabled: (enabled=true) -> - if @localStream? - @media.desktop = enabled - delete @localStream - @getLocalUserMedia (err) => - if err? - return - @screenShareEnabled.set enabled - @stopAllPeerConnections() - @joinCall() - - disableVideo: -> - @setVideoEnabled false - - enableVideo: -> - @setVideoEnabled true - - stop: -> - @active = false - @monitor = false - @remoteMonitoring = false - if @localStream? and typeof @localStream isnt 'undefined' - @localStream.getTracks().forEach (track) -> - track.stop() - @localUrl.set undefined - delete @localStream - - @stopAllPeerConnections() - - - ### - @param media {Object} - audio {Boolean} - video {Boolean} - ### - startCall: (media={}) -> - @log 'startCall', arguments - @media = media - @getLocalUserMedia => - @active = true - @transport.startCall - media: @media - - startCallAsMonitor: (media={}) -> - @log 'startCallAsMonitor', arguments - @media = media - @active = true - @monitor = true - @transport.startCall - media: @media - monitor: true - - - ### - @param data {Object} - from {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - ### - onRemoteCall: (data) -> - if @autoAccept is true - FlowRouter.goToRoomById data.room - Meteor.defer => - @joinCall - to: data.from - monitor: data.monitor - media: data.media - return - - fromUsername = Meteor.users.findOne(data.from)?.username - subscription = ChatSubscription.findOne({rid: data.room}) - - if data.monitor is true - icon = 'eye' - title = "Monitor call from #{fromUsername}" - else if subscription?.t is 'd' - if data.media?.video - icon = 'videocam' - title = "Direct video call from #{fromUsername}" - else - icon = 'phone' - title = "Direct audio call from #{fromUsername}" - else - if data.media?.video - icon = 'videocam' - title = "Group video call from #{subscription.name}" - else - icon = 'phone' - title = "Group audio call from #{subscription.name}" - - swal - title: "<i class='icon-#{icon} alert-icon success-color'></i>#{title}" - text: "Do you want to accept?" - html: true - showCancelButton: true - confirmButtonText: "Yes" - cancelButtonText: "No" - , (isConfirm) => - if isConfirm - FlowRouter.goToRoomById data.room - Meteor.defer => - @joinCall - to: data.from - monitor: data.monitor - media: data.media - else - @stop() - - - ### - @param data {Object} - to {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - ### - joinCall: (data={}) -> - if data.media?.audio? - @media.audio = data.media.audio - - if data.media?.video? - @media.video = data.media.video - - data.media = @media - - @log 'joinCall', arguments - @getLocalUserMedia => - @remoteMonitoring = data.monitor - @active = true - @transport.joinCall(data) - - - ### - @param data {Object} - from {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - ### - onRemoteJoin: (data) -> - if @active isnt true then return - - @log 'onRemoteJoin', arguments - - peerConnection = @getPeerConnection data.from - - # needsRefresh = false - # if peerConnection.iceConnectionState isnt 'new' - # needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true - # needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true - # needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop - - # if peerConnection.signalingState is "have-local-offer" or needsRefresh - if peerConnection.signalingState isnt "checking" - @stopPeerConnection data.from - peerConnection = @getPeerConnection data.from - - if peerConnection.iceConnectionState isnt 'new' - return - - peerConnection.remoteMedia = data.media - - peerConnection.addStream @localStream if @localStream - - onOffer = (offer) => - onLocalDescription = => - @transport.sendDescription - to: data.from - type: 'offer' - ts: peerConnection.createdAt - media: @media - description: - sdp: offer.sdp - type: offer.type - - peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, @onError) - - if data.monitor is true - peerConnection.createOffer onOffer, @onError, - mandatory: - OfferToReceiveAudio: data.media.audio - OfferToReceiveVideo: data.media.video - else - peerConnection.createOffer(onOffer, @onError) - - - ### - @param data {Object} - from {String} - ts {Integer} - description {String} - ### - onRemoteOffer: (data) -> - if @active isnt true then return - - @log 'onRemoteOffer', arguments - peerConnection = @getPeerConnection data.from - - if peerConnection.signalingState in ["have-local-offer", "stable"] and peerConnection.createdAt < data.ts - @stopPeerConnection data.from - peerConnection = @getPeerConnection data.from - - if peerConnection.iceConnectionState isnt 'new' - return - - peerConnection.setRemoteDescription new RTCSessionDescription(data.description) - - try peerConnection.addStream @localStream if @localStream - - onAnswer = (answer) => - onLocalDescription = => - @transport.sendDescription - to: data.from - type: 'answer' - ts: peerConnection.createdAt - description: - sdp: answer.sdp - type: answer.type - - peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, @onError) - - peerConnection.createAnswer(onAnswer, @onError) - - - ### - @param data {Object} - to {String} - from {String} - candidate {RTCIceCandidate JSON encoded} - ### - onRemoteCandidate: (data) -> - if @active isnt true then return - if data.to isnt @selfId then return - - @log 'onRemoteCandidate', arguments - peerConnection = @getPeerConnection data.from - - if peerConnection.iceConnectionState not in ["closed", "failed", "disconnected", "completed"] - peerConnection.addIceCandidate new RTCIceCandidate(data.candidate) - - - ### - @param data {Object} - to {String} - from {String} - type {String} [offer, answer] - description {RTCSessionDescription JSON encoded} - ts {Integer} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - ### - onRemoteDescription: (data) -> - if @active isnt true then return - if data.to isnt @selfId then return - - @log 'onRemoteDescription', arguments - peerConnection = @getPeerConnection data.from - - if data.type is 'offer' - peerConnection.remoteMedia = data.media - @onRemoteOffer - from: data.from - ts: data.ts - description: data.description - else - peerConnection.setRemoteDescription new RTCSessionDescription(data.description) - - -WebRTC = new class - constructor: -> - @instancesByRoomId = {} - - getInstanceByRoomId: (roomId) -> - subscription = ChatSubscription.findOne({rid: roomId}) - if not subscription - return - - enabled = false - switch subscription.t - when 'd' - enabled = RocketChat.settings.get('WebRTC_Enable_Direct') - when 'p' - enabled = RocketChat.settings.get('WebRTC_Enable_Private') - when 'c' - enabled = RocketChat.settings.get('WebRTC_Enable_Channel') - - if enabled is false - return - - if not @instancesByRoomId[roomId]? - @instancesByRoomId[roomId] = new WebRTCClass Meteor.userId(), roomId - - return @instancesByRoomId[roomId] - - -Meteor.startup -> - Tracker.autorun -> - if Meteor.userId() - RocketChat.Notifications.onUser 'webrtc', (type, data) => - if not data.room? then return - - webrtc = WebRTC.getInstanceByRoomId(data.room) - - webrtc.transport.onUserStream type, data diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.js b/packages/rocketchat-webrtc/client/WebRTCClass.js new file mode 100644 index 0000000000000000000000000000000000000000..dfbccbd4391f6c16874c8440c7ebd9e23e8c378b --- /dev/null +++ b/packages/rocketchat-webrtc/client/WebRTCClass.js @@ -0,0 +1,992 @@ +/* globals chrome, ChromeScreenShare */ +class WebRTCTransportClass { + constructor(webrtcInstance) { + this.debug = false; + this.webrtcInstance = webrtcInstance; + this.callbacks = {}; + RocketChat.Notifications.onRoom(this.webrtcInstance.room, 'webrtc', (type, data) => { + const onRemoteStatus = this.callbacks['onRemoteStatus']; + this.log('WebRTCTransportClass - onRoom', type, data); + switch (type) { + case 'status': + if (onRemoteStatus && onRemoteStatus.length) { + onRemoteStatus.forEach(fn => fn(data)); + } + } + }); + } + + log() { + if (this.debug === true) { + console.log.apply(console, arguments); + } + } + + onUserStream(type, data) { + if (data.room !== this.webrtcInstance.room) { + return; + } + this.log('WebRTCTransportClass - onUser', type, data); + const onRemoteCall = this.callbacks['onRemoteCall']; + const onRemoteJoin = this.callbacks['onRemoteJoin']; + const onRemoteCandidate = this.callbacks['onRemoteCandidate']; + const onRemoteDescription = this.callbacks['onRemoteDescription']; + + switch (type) { + case 'call': + if (onRemoteCall && onRemoteCall.length) { + onRemoteCall.forEach(fn => fn(data)); + } + break; + case 'join': + if (onRemoteJoin && onRemoteJoin.length) { + onRemoteJoin.forEach(fn => fn(data)); + } + break; + case 'candidate': + if (onRemoteCandidate && onRemoteCandidate.length) { + onRemoteCandidate.forEach(fn => fn(data)); + } + break; + case 'description': + if (onRemoteDescription && onRemoteDescription.length) { + onRemoteDescription.forEach(fn => fn(data)); + } + } + } + + startCall(data) { + this.log('WebRTCTransportClass - startCall', this.webrtcInstance.room, this.webrtcInstance.selfId); + RocketChat.Notifications.notifyUsersOfRoom(this.webrtcInstance.room, 'webrtc', 'call', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } + + joinCall(data) { + this.log('WebRTCTransportClass - joinCall', this.webrtcInstance.room, this.webrtcInstance.selfId); + if (data.monitor === true) { + RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'join', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } else { + RocketChat.Notifications.notifyUsersOfRoom(this.webrtcInstance.room, 'webrtc', 'join', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } + } + + sendCandidate(data) { + data.from = this.webrtcInstance.selfId; + data.room = this.webrtcInstance.room; + this.log('WebRTCTransportClass - sendCandidate', data); + RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'candidate', data); + } + + sendDescription(data) { + data.from = this.webrtcInstance.selfId; + data.room = this.webrtcInstance.room; + this.log('WebRTCTransportClass - sendDescription', data); + RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'description', data); + } + + sendStatus(data) { + this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room); + data.from = this.webrtcInstance.selfId; + RocketChat.Notifications.notifyRoom(this.webrtcInstance.room, 'webrtc', 'status', data); + } + + onRemoteCall(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteCall'] == null) { + callbacks['onRemoteCall'] = []; + } + callbacks['onRemoteCall'].push(fn); + } + + onRemoteJoin(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteJoin'] == null) { + callbacks['onRemoteJoin'] = []; + } + callbacks['onRemoteJoin'].push(fn); + } + + onRemoteCandidate(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteCandidate'] == null) { + callbacks['onRemoteCandidate'] = []; + } + callbacks['onRemoteCandidate'].push(fn); + } + + onRemoteDescription(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteDescription'] == null) { + callbacks['onRemoteDescription'] = []; + } + callbacks['onRemoteDescription'].push(fn); + } + + onRemoteStatus(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteStatus'] == null) { + callbacks['onRemoteStatus'] = []; + } + callbacks['onRemoteStatus'].push(fn); + } + + + +} + +class WebRTCClass { + /* + @param seldId {String} + @param room {String} + */ + + constructor(selfId, room) { + this.config = { + iceServers: [] + }; + this.debug = false; + this.TransportClass = WebRTCTransportClass; + this.selfId = selfId; + this.room = room; + let servers = RocketChat.settings.get('WebRTC_Servers'); + if (servers && servers.trim() !== '') { + servers = servers.replace(/\s/g, ''); + servers = servers.split(','); + + servers.forEach(server => { + server = server.split('@'); + const serverConfig = { + urls: server.pop() + }; + if (server.length === 1) { + server = server[0].split(':'); + serverConfig.username = decodeURIComponent(server[0]); + serverConfig.credential = decodeURIComponent(server[1]); + } + this.config.iceServers.push(serverConfig); + }); + } + this.peerConnections = {}; + this.remoteItems = new ReactiveVar([]); + this.remoteItemsById = new ReactiveVar({}); + this.callInProgress = new ReactiveVar(false); + this.audioEnabled = new ReactiveVar(true); + this.videoEnabled = new ReactiveVar(true); + this.overlayEnabled = new ReactiveVar(false); + this.screenShareEnabled = new ReactiveVar(false); + this.localUrl = new ReactiveVar; + this.active = false; + this.remoteMonitoring = false; + this.monitor = false; + this.autoAccept = false; + this.navigator = undefined; + const userAgent = navigator.userAgent.toLocaleLowerCase(); + + if (userAgent.indexOf('electron') !== -1) { + this.navigator = 'electron'; + } else if (userAgent.indexOf('chrome') !== -1) { + this.navigator = 'chrome'; + } else if (userAgent.indexOf('firefox') !== -1) { + this.navigator = 'firefox'; + } else if (userAgent.indexOf('safari') !== -1) { + this.navigator = 'safari'; + } + const nav = this.navigator; + this.screenShareAvailable = nav === 'chrome' || nav === 'firefox' || nav === 'electron'; + this.media = { + video: false, + audio: true + }; + this.transport = new this.TransportClass(this); + this.transport.onRemoteCall(this.onRemoteCall.bind(this)); + this.transport.onRemoteJoin(this.onRemoteJoin.bind(this)); + this.transport.onRemoteCandidate(this.onRemoteCandidate.bind(this)); + this.transport.onRemoteDescription(this.onRemoteDescription.bind(this)); + this.transport.onRemoteStatus(this.onRemoteStatus.bind(this)); + Meteor.setInterval(this.checkPeerConnections.bind(this), 1000); + + //Meteor.setInterval(this.broadcastStatus.bind(@), 1000); + } + + log() { + if (this.debug === true) { + console.log.apply(console, arguments); + } + } + + onError() { + console.error.apply(console, arguments); + } + + checkPeerConnections() { + const peerConnections = this.peerConnections; + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + if (peerConnection.iceConnectionState !== 'connected' && peerConnection.iceConnectionState !== 'completed' && peerConnection.createdAt + 5000 < Date.now()) { + this.stopPeerConnection(id); + } + }); + } + + updateRemoteItems() { + const items = []; + const itemsById = {}; + const peerConnections = this.peerConnections; + + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + + peerConnection.getRemoteStreams().forEach(remoteStream => { + const item = { + id, + url: URL.createObjectURL(remoteStream), + state: peerConnection.iceConnectionState + }; + switch (peerConnection.iceConnectionState) { + case 'checking': + item.stateText = 'Connecting...'; + break; + case 'connected': + case 'completed': + item.stateText = 'Connected'; + item.connected = true; + break; + case 'disconnected': + item.stateText = 'Disconnected'; + break; + case 'failed': + item.stateText = 'Failed'; + break; + case 'closed': + item.stateText = 'Closed'; + } + items.push(item); + itemsById[id] = item; + }); + }); + this.remoteItems.set(items); + this.remoteItemsById.set(itemsById); + } + + resetCallInProgress() { + this.callInProgress.set(false); + } + + broadcastStatus() { + if (this.active !== true || this.monitor === true || this.remoteMonitoring === true) { + return; + } + const remoteConnections = []; + const peerConnections = this.peerConnections; + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + remoteConnections.push({ + id, + media: peerConnection.remoteMedia + }); + }); + + this.transport.sendStatus({ + media: this.media, + remoteConnections + }); + } + + + /* + @param data {Object} + from {String} + media {Object} + remoteConnections {Array[Object]} + id {String} + media {Object} + */ + + onRemoteStatus(data) { + //this.log(onRemoteStatus, arguments); + this.callInProgress.set(true); + Meteor.clearTimeout(this.callInProgressTimeout); + this.callInProgressTimeout = Meteor.setTimeout(this.resetCallInProgress.bind(this), 2000); + if (this.active !== true) { + return; + } + const remoteConnections = [{ + id: data.from, + media: data.media + }, + ...data.remoteConnections]; + + remoteConnections.forEach(remoteConnection => { + if (remoteConnection.id !== this.selfId && (this.peerConnections[remoteConnection.id] == null)) { + this.log('reconnecting with', remoteConnection.id); + this.onRemoteJoin({ + from: remoteConnection.id, + media: remoteConnection.media + }); + } + }); + } + + + /* + @param id {String} + */ + + getPeerConnection(id) { + if (this.peerConnections[id] != null) { + return this.peerConnections[id]; + } + const peerConnection = new RTCPeerConnection(this.config); + + peerConnection.createdAt = Date.now(); + peerConnection.remoteMedia = {}; + this.peerConnections[id] = peerConnection; + const eventNames = ['icecandidate', 'addstream', 'removestream', 'iceconnectionstatechange', 'datachannel', 'identityresult', 'idpassertionerror', 'idpvalidationerror', 'negotiationneeded', 'peeridentity', 'signalingstatechange']; + + eventNames.forEach(eventName => { + peerConnection.addEventListener(eventName, (e) => { + this.log(id, e.type, e); + }); + }); + + peerConnection.addEventListener('icecandidate', (e) => { + if (e.candidate == null) { + return; + } + this.transport.sendCandidate({ + to: id, + candidate: { + candidate: e.candidate.candidate, + sdpMLineIndex: e.candidate.sdpMLineIndex, + sdpMid: e.candidate.sdpMid + } + }); + }); + peerConnection.addEventListener('addstream', () => { + this.updateRemoteItems(); + }); + peerConnection.addEventListener('removestream', () => { + this.updateRemoteItems(); + }); + peerConnection.addEventListener('iceconnectionstatechange', () => { + if ((peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'closed') && peerConnection === this.peerConnections[id]) { + this.stopPeerConnection(id); + Meteor.setTimeout(() => { + if (Object.keys(this.peerConnections).length === 0) { + this.stop(); + } + }, 3000); + } + this.updateRemoteItems(); + }); + return peerConnection; + } + + _getUserMedia(media, onSuccess, onError) { + const onSuccessLocal = function(stream) { + if (AudioContext && stream.getAudioTracks().length > 0) { + const audioContext = new AudioContext; + const source = audioContext.createMediaStreamSource(stream); + const volume = audioContext.createGain(); + source.connect(volume); + const peer = audioContext.createMediaStreamDestination(); + volume.connect(peer); + volume.gain.value = 0.6; + stream.removeTrack(stream.getAudioTracks()[0]); + stream.addTrack(peer.stream.getAudioTracks()[0]); + stream.volume = volume; + this.audioContext = audioContext; + } + onSuccess(stream); + }; + navigator.getUserMedia(media, onSuccessLocal, onError); + } + + getUserMedia(media, onSuccess, onError = this.onError) { + if (media.desktop !== true) { + this._getUserMedia(media, onSuccess, onError); + return; + } + if (this.screenShareAvailable !== true) { + console.log('Screen share is not avaliable'); + return; + } + const getScreen = (audioStream) => { + if (document.cookie.indexOf('rocketchatscreenshare=chrome') === -1 && (window.rocketchatscreenshare == null) && this.navigator !== 'electron') { + const refresh = function() { + swal({ + type: 'warning', + title: TAPi18n.__('Refresh_your_page_after_install_to_enable_screen_sharing') + }); + }; + swal({ + type: 'warning', + title: TAPi18n.__('Screen_Share'), + text: TAPi18n.__('You_need_install_an_extension_to_allow_screen_sharing'), + html: true, + showCancelButton: true, + confirmButtonText: TAPi18n.__('Install_Extension'), + cancelButtonText: TAPi18n.__('Cancel') + }, (isConfirm) => { + if (isConfirm) { + if (this.navigator === 'chrome') { + const url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf'; + try { + chrome.webstore.install(url, refresh, function() { + window.open(url); + refresh(); + }); + } catch (_error) { + console.log(_error); + } + } else if (this.navigator === 'firefox') { + window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/'); + refresh(); + } + } + }); + return onError(false); + } + const getScreenSuccess = (stream) => { + if (audioStream != null) { + stream.addTrack(audioStream.getAudioTracks()[0]); + } + onSuccess(stream); + }; + if (this.navigator === 'firefox') { + media = { + audio: media.audio, + video: { + mozMediaSource: 'window', + mediaSource: 'window' + } + }; + this._getUserMedia(media, getScreenSuccess, onError); + } else { + ChromeScreenShare.getSourceId(this.navigator, (id) => { + media = { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: id, + maxWidth: 1280, + maxHeight: 720 + } + } + }; + this._getUserMedia(media, getScreenSuccess, onError); + }); + } + }; + if (this.navigator === 'firefox' || (media.audio == null) || media.audio === false) { + getScreen(); + } else { + const getAudioSuccess = (audioStream) => { + getScreen(audioStream); + }; + const getAudioError = () => { + getScreen(); + }; + + this._getUserMedia({ + audio: media.audio + }, getAudioSuccess, getAudioError); + } + } + + + /* + @param callback {Function} + */ + + getLocalUserMedia(callback) { + this.log('getLocalUserMedia', arguments); + if (this.localStream != null) { + return callback(null, this.localStream); + } + const onSuccess = (stream) => { + this.localStream = stream; + this.localUrl.set(URL.createObjectURL(stream)); + this.videoEnabled.set(this.media.video === true); + this.audioEnabled.set(this.media.audio === true); + const peerConnections = this.peerConnections; + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + peerConnection.addStream(stream); + }); + callback(null, this.localStream); + }; + const onError = (error) => { + callback(false); + this.onError(error); + }; + this.getUserMedia(this.media, onSuccess, onError); + } + + + /* + @param id {String} + */ + + stopPeerConnection(id) { + const peerConnection = this.peerConnections[id]; + if (peerConnection == null) { + return; + } + delete this.peerConnections[id]; + peerConnection.close(); + this.updateRemoteItems(); + } + + stopAllPeerConnections() { + const peerConnections = this.peerConnections; + + Object.keys(peerConnections).forEach(id => { + this.stopPeerConnection(id); + }); + + window.audioContext && window.audioContext.close(); + } + + setAudioEnabled(enabled = true) { + if (this.localStream != null) { + if (enabled === true && this.media.audio !== true) { + delete this.localStream; + this.media.audio = true; + this.getLocalUserMedia(() => { + this.stopAllPeerConnections(); + this.joinCall(); + }); + } else { + this.localStream.getAudioTracks().forEach(function(audio) { + audio.enabled = enabled; + }); + this.audioEnabled.set(enabled); + } + } + } + + disableAudio() { + this.setAudioEnabled(false); + } + + enableAudio() { + this.setAudioEnabled(true); + } + + setVideoEnabled(enabled = true) { + if (this.localStream != null) { + if (enabled === true && this.media.video !== true) { + delete this.localStream; + this.media.video = true; + this.getLocalUserMedia(() => { + this.stopAllPeerConnections(); + this.joinCall(); + }); + } else { + this.localStream.getVideoTracks().forEach(function(video) { + video.enabled = enabled; + }); + this.videoEnabled.set(enabled); + } + } + } + + disableScreenShare() { + this.setScreenShareEnabled(false); + } + + enableScreenShare() { + this.setScreenShareEnabled(true); + } + + setScreenShareEnabled(enabled = true) { + if (this.localStream != null) { + this.media.desktop = enabled; + delete this.localStream; + this.getLocalUserMedia(err => { + if (err != null) { + return; + } + this.screenShareEnabled.set(enabled); + this.stopAllPeerConnections(); + this.joinCall(); + }); + } + } + + disableVideo() { + this.setVideoEnabled(false); + } + + enableVideo() { + this.setVideoEnabled(true); + } + + stop() { + this.active = false; + this.monitor = false; + this.remoteMonitoring = false; + if (this.localStream != null && typeof this.localStream !== 'undefined') { + this.localStream.getTracks().forEach(track => track.stop()); + } + this.localUrl.set(undefined); + delete this.localStream; + this.stopAllPeerConnections(); + } + + + /* + @param media {Object} + audio {Boolean} + video {Boolean} + */ + + startCall(media = {}) { + this.log('startCall', arguments); + this.media = media; + this.getLocalUserMedia(() => { + this.active = true; + this.transport.startCall({ + media: this.media + }); + }); + } + + startCallAsMonitor(media = {}) { + this.log('startCallAsMonitor', arguments); + this.media = media; + this.active = true; + this.monitor = true; + this.transport.startCall({ + media: this.media, + monitor: true + }); + } + + + /* + @param data {Object} + from {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + */ + + onRemoteCall(data) { + if (this.autoAccept === true) { + FlowRouter.goToRoomById(data.room); + Meteor.defer(() => { + this.joinCall({ + to: data.from, + monitor: data.monitor, + media: data.media + }); + }); + return; + } + + const user = Meteor.users.findOne(data.from); + let fromUsername = undefined; + if (user && user.username) { + fromUsername = user.username; + } + const subscription = ChatSubscription.findOne({ + rid: data.room + }); + + let icon; + let title; + if (data.monitor === true) { + icon = 'eye'; + title = `Monitor call from ${ fromUsername }`; + } else if (subscription && subscription.t === 'd') { + if (data.media && data.media.video) { + icon = 'videocam'; + title = `Direct video call from ${ fromUsername }`; + } else { + icon = 'phone'; + title = `Direct audio call from ${ fromUsername }`; + } + } else if (data.media && data.media.video) { + icon = 'videocam'; + title = `Group video call from ${ subscription.name }`; + } else { + icon = 'phone'; + title = `Group audio call from ${ subscription.name }`; + } + swal({ + title: `<i class='icon-${ icon } alert-icon success-color'></i>${ title }`, + text: 'Do you want to accept?', + html: true, + showCancelButton: true, + confirmButtonText: 'Yes', + cancelButtonText: 'No' + }, (isConfirm) => { + if (isConfirm) { + FlowRouter.goToRoomById(data.room); + Meteor.defer(() => { + this.joinCall({ + to: data.from, + monitor: data.monitor, + media: data.media + }); + }); + } else { + this.stop(); + } + }); + } + + + /* + @param data {Object} + to {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + joinCall(data = {}) { + if (data.media && data.media.audio) { + this.media.audio = data.media.audio; + } + if (data.media && data.media.video) { + this.media.video = data.media.video; + } + data.media = this.media; + this.log('joinCall', arguments); + this.getLocalUserMedia(() => { + this.remoteMonitoring = data.monitor; + this.active = true; + this.transport.joinCall(data); + }); + } + + + onRemoteJoin(data) { + if (this.active !== true) { + return; + } + this.log('onRemoteJoin', arguments); + let peerConnection = this.getPeerConnection(data.from); + + // needsRefresh = false + // if peerConnection.iceConnectionState isnt 'new' + // needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true + // needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true + // needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop + + // # if peerConnection.signalingState is "have-local-offer" or needsRefresh + + if (peerConnection.signalingState !== 'checking') { + this.stopPeerConnection(data.from); + peerConnection = this.getPeerConnection(data.from); + } + if (peerConnection.iceConnectionState !== 'new') { + return; + } + peerConnection.remoteMedia = data.media; + if (this.localStream) { + peerConnection.addStream(this.localStream); + } + const onOffer = offer => { + const onLocalDescription = () => { + this.transport.sendDescription({ + to: data.from, + type: 'offer', + ts: peerConnection.createdAt, + media: this.media, + description: { + sdp: offer.sdp, + type: offer.type + } + }); + }; + + peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); + }; + + if (data.monitor === true) { + peerConnection.createOffer(onOffer, this.onError, { + mandatory: { + OfferToReceiveAudio: data.media.audio, + OfferToReceiveVideo: data.media.video + } + }); + } else { + peerConnection.createOffer(onOffer, this.onError); + } + } + + + + onRemoteOffer(data) { + if (this.active !== true) { + return; + } + + this.log('onRemoteOffer', arguments); + let peerConnection = this.getPeerConnection(data.from); + + if (['have-local-offer', 'stable'].includes(peerConnection.signalingState) && (peerConnection.createdAt < data.ts)) { + this.stopPeerConnection(data.from); + peerConnection = this.getPeerConnection(data.from); + } + + if (peerConnection.iceConnectionState !== 'new') { + return; + } + + peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + + try { + if (this.localStream) { + peerConnection.addStream(this.localStream); + } + } catch (error) { + console.log(error); + } + + const onAnswer = answer => { + const onLocalDescription = () => { + this.transport.sendDescription({ + to: data.from, + type: 'answer', + ts: peerConnection.createdAt, + description: { + sdp: answer.sdp, + type: answer.type + } + }); + }; + + peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); + }; + + peerConnection.createAnswer(onAnswer, this.onError); + } + + + /* + @param data {Object} + to {String} + from {String} + candidate {RTCIceCandidate JSON encoded} + */ + + onRemoteCandidate(data) { + if (this.active !== true) { + return; + } + if (data.to !== this.selfId) { + return; + } + this.log('onRemoteCandidate', arguments); + const peerConnection = this.getPeerConnection(data.from); + if (peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed') { + peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); + } + } + + + /* + @param data {Object} + to {String} + from {String} + type {String} [offer, answer] + description {RTCSessionDescription JSON encoded} + ts {Integer} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + onRemoteDescription(data) { + if (this.active !== true) { + return; + } + if (data.to !== this.selfId) { + return; + } + this.log('onRemoteDescription', arguments); + const peerConnection = this.getPeerConnection(data.from); + if (data.type === 'offer') { + peerConnection.remoteMedia = data.media; + this.onRemoteOffer({ + from: data.from, + ts: data.ts, + description: data.description + }); + } else { + peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + } + } + +} + +const WebRTC = new class { + constructor() { + this.instancesByRoomId = {}; + } + + getInstanceByRoomId(roomId) { + const subscription = ChatSubscription.findOne({ rid: roomId }); + if (!subscription) { + return; + } + let enabled = false; + switch (subscription.t) { + case 'd': + enabled = RocketChat.settings.get('WebRTC_Enable_Direct'); + break; + case 'p': + enabled = RocketChat.settings.get('WebRTC_Enable_Private'); + break; + case 'c': + enabled = RocketChat.settings.get('WebRTC_Enable_Channel'); + } + if (enabled === false) { + return; + } + if (this.instancesByRoomId[roomId] == null) { + this.instancesByRoomId[roomId] = new WebRTCClass(Meteor.userId(), roomId); + } + return this.instancesByRoomId[roomId]; + } +}; + +Meteor.startup(function() { + Tracker.autorun(function() { + if (Meteor.userId()) { + RocketChat.Notifications.onUser('webrtc', (type, data) => { + if (data.room == null) { + return; + } + const webrtc = WebRTC.getInstanceByRoomId(data.room); + webrtc.transport.onUserStream(type, data); + }); + } + }); +}); + +export {WebRTC}; diff --git a/packages/rocketchat-webrtc/adapter.js b/packages/rocketchat-webrtc/client/adapter.js similarity index 100% rename from packages/rocketchat-webrtc/adapter.js rename to packages/rocketchat-webrtc/client/adapter.js diff --git a/packages/rocketchat-webrtc/client/screenShare.js b/packages/rocketchat-webrtc/client/screenShare.js new file mode 100644 index 0000000000000000000000000000000000000000..22f9ba2dc951e3b506f2003f104ea44499d67e4a --- /dev/null +++ b/packages/rocketchat-webrtc/client/screenShare.js @@ -0,0 +1,29 @@ +/* globals ChromeScreenShare, fireGlobalEvent */ +this.ChromeScreenShare = { + screenCallback: undefined, + getSourceId(navigator, callback) { + if (callback == null) { + throw '"callback" parameter is mandatory.'; + } + ChromeScreenShare.screenCallback = callback; + if (navigator === 'electron') { + return fireGlobalEvent('get-sourceId', '*'); + } + return window.postMessage('get-sourceId', '*'); + } +}; + +window.addEventListener('message', function(e) { + if (e.origin !== window.location.origin) { + return; + } + if (e.data === 'PermissionDeniedError') { + if (ChromeScreenShare.screenCallback != null) { + return ChromeScreenShare.screenCallback('PermissionDeniedError'); + } + throw new Error('PermissionDeniedError'); + } + if (e.data.sourceId != null) { + return typeof ChromeScreenShare.screenCallback === 'function' && ChromeScreenShare.screenCallback(e.data.sourceId); + } +}); diff --git a/packages/rocketchat-webrtc/package.js b/packages/rocketchat-webrtc/package.js index 53d56d1cde7020237cae4eecaf24cea53909d350..608f3d91768ef973a71a229d4fea6089851e4a18 100644 --- a/packages/rocketchat-webrtc/package.js +++ b/packages/rocketchat-webrtc/package.js @@ -7,16 +7,15 @@ Package.describe({ Package.onUse(function(api) { api.use('rocketchat:lib'); - api.use('coffeescript'); api.use('ecmascript'); api.use('templating', 'client'); + api.mainModule('client/WebRTCClass.js', 'client'); + api.addFiles('client/adapter.js', 'client'); + // api.addFiles('); + api.addFiles('client/screenShare.js', 'client'); - api.addFiles('adapter.js', 'client'); - api.addFiles('WebRTCClass.coffee', 'client'); - api.addFiles('screenShare.coffee', 'client'); + api.addFiles('server/settings.js', 'server'); - api.addFiles('server/settings.coffee', 'server'); - - api.export('WebRTC'); + api.export('WebRTC', 'client'); }); diff --git a/packages/rocketchat-webrtc/screenShare.coffee b/packages/rocketchat-webrtc/screenShare.coffee deleted file mode 100644 index c157e7c045b3eaa5c44e28f8c7541bbf3073e764..0000000000000000000000000000000000000000 --- a/packages/rocketchat-webrtc/screenShare.coffee +++ /dev/null @@ -1,27 +0,0 @@ -@ChromeScreenShare = - screenCallback: undefined - - getSourceId: (navigator, callback) -> - if not callback? then throw '"callback" parameter is mandatory.' - - ChromeScreenShare.screenCallback = callback - - if navigator is 'electron' - fireGlobalEvent('get-sourceId', '*') - else - window.postMessage('get-sourceId', '*') - -window.addEventListener 'message', (e) -> - if e.origin isnt window.location.origin - return - - # "cancel" button was clicked - if e.data is 'PermissionDeniedError' - if ChromeScreenShare.screenCallback? - return ChromeScreenShare.screenCallback('PermissionDeniedError') - else - throw new Error('PermissionDeniedError') - - # extension shared temp sourceId - if e.data.sourceId? - ChromeScreenShare.screenCallback?(e.data.sourceId) diff --git a/packages/rocketchat-webrtc/server/settings.coffee b/packages/rocketchat-webrtc/server/settings.coffee deleted file mode 100644 index 84337ffbc49c9bab26aeaee854b463f412ec6ac7..0000000000000000000000000000000000000000 --- a/packages/rocketchat-webrtc/server/settings.coffee +++ /dev/null @@ -1,5 +0,0 @@ -RocketChat.settings.addGroup 'WebRTC', -> - @add 'WebRTC_Enable_Channel', false, { type: 'boolean', group: 'WebRTC', public: true} - @add 'WebRTC_Enable_Private', true , { type: 'boolean', group: 'WebRTC', public: true} - @add 'WebRTC_Enable_Direct' , true , { type: 'boolean', group: 'WebRTC', public: true} - @add 'WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', { type: 'string', group: 'WebRTC', public: true} diff --git a/packages/rocketchat-webrtc/server/settings.js b/packages/rocketchat-webrtc/server/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..c32f3103acaf04b62876fa0d16f37d36e19f9490 --- /dev/null +++ b/packages/rocketchat-webrtc/server/settings.js @@ -0,0 +1,22 @@ +RocketChat.settings.addGroup('WebRTC', function() { + this.add('WebRTC_Enable_Channel', false, { + type: 'boolean', + group: 'WebRTC', + 'public': true + }); + this.add('WebRTC_Enable_Private', true, { + type: 'boolean', + group: 'WebRTC', + 'public': true + }); + this.add('WebRTC_Enable_Direct', true, { + type: 'boolean', + group: 'WebRTC', + 'public': true + }); + return this.add('WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', { + type: 'string', + group: 'WebRTC', + 'public': true + }); +}); diff --git a/server/configuration/accounts_meld.js b/server/configuration/accounts_meld.js index 1848635c4a5ea9c1d3633014842e19a1db472bfc..8eee62d4f85589ad7cc5e3826f8555dc01bfafd7 100644 --- a/server/configuration/accounts_meld.js +++ b/server/configuration/accounts_meld.js @@ -18,7 +18,8 @@ Accounts.updateOrCreateUserFromExternalService = function(serviceName, serviceDa if (serviceName === 'meteor-developer') { if (Array.isArray(serviceData.emails)) { - serviceData.email = serviceData.emails.sort(a => a.primary !== true).filter(item => item.verified === true)[0]; + const primaryEmail = serviceData.emails.sort(a => a.primary !== true).filter(item => item.verified === true)[0]; + serviceData.email = primaryEmail && primaryEmail.address; } } diff --git a/server/startup/migrations/v094.js b/server/startup/migrations/v094.js new file mode 100644 index 0000000000000000000000000000000000000000..4cc141cb8d8d81ce17fc5b0cc7ccd12def3a2999 --- /dev/null +++ b/server/startup/migrations/v094.js @@ -0,0 +1,26 @@ +RocketChat.Migrations.add({ + version: 94, + up() { + const query = { + 'emails.address.address': { $exists: true } + }; + + RocketChat.models.Users.find(query, {'emails.address.address': 1}).forEach((user) => { + let emailAddress; + user.emails.some(email => { + if (email.address && email.address.address) { + emailAddress = email.address.address; + return true; + } + }); + RocketChat.models.Users.update({ + _id: user._id, + 'emails.address.address': emailAddress + }, { + $set: { + 'emails.$.address': emailAddress + } + }); + }); + } +}); diff --git a/server/startup/presence.js b/server/startup/presence.js index f8248e9c3d7b77e31db3412a58d37edfe805e538..59d2a3c94a069a488f4b46114cd8752d7d43f21a 100644 --- a/server/startup/presence.js +++ b/server/startup/presence.js @@ -3,11 +3,11 @@ Meteor.startup(function() { const instance = { host: 'localhost', - port: process.env.PORT + port: String(process.env.PORT).trim() }; if (process.env.INSTANCE_IP) { - instance.host = process.env.INSTANCE_IP; + instance.host = String(process.env.INSTANCE_IP).trim(); } InstanceStatus.registerInstance('rocket.chat', instance); diff --git a/server/stream/streamBroadcast.js b/server/stream/streamBroadcast.js index 6de334e34b95565b97435f040812b28d5fcf95fc..321bdc72b54047182e1ebf6afc0b5153083cd401 100644 --- a/server/stream/streamBroadcast.js +++ b/server/stream/streamBroadcast.js @@ -2,6 +2,9 @@ import {DDPCommon} from 'meteor/ddp-common'; +process.env.PORT = String(process.env.PORT).trim(); +process.env.INSTANCE_IP = String(process.env.INSTANCE_IP).trim(); + const connections = {}; this.connections = connections; @@ -257,3 +260,18 @@ function startStreamBroadcast() { Meteor.startup(function() { return startStreamBroadcast(); }); + +Meteor.methods({ + 'instances/get'() { + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'view-statistics')) { + throw new Meteor.Error('error-action-not-allowed', 'List instances is not allowed', { + method: 'instances/get' + }); + } + + return Object.keys(connections).map(address => { + const conn = connections[address]; + return Object.assign({ address, currentStatus: conn._stream.currentStatus }, _.pick(conn, 'instanceRecord', 'broadcastAuth')); + }); + } +}); diff --git a/tests/end-to-end/ui/09-channel.js b/tests/end-to-end/ui/09-channel.js index 1b7bf7d2870869e3b4e3ceb6cf1424e6a60546a7..2a852d12589c999409113941887ef8d2ce2f8e41 100644 --- a/tests/end-to-end/ui/09-channel.js +++ b/tests/end-to-end/ui/09-channel.js @@ -37,6 +37,7 @@ describe('channel', ()=> { it('should start a direct message with rocket.cat', () => { sideNav.searchChannel('rocket.cat'); + mainContent.channelTitle.waitForVisible(5000); mainContent.channelTitle.getText().should.equal('rocket.cat'); }); }); diff --git a/tests/pageobjects/main-content.page.js b/tests/pageobjects/main-content.page.js index beee3e77ed56e917e3472d0094fc66d37db2b953..0a672a9eabbe75ff15449e125aa9177284f7c88f 100644 --- a/tests/pageobjects/main-content.page.js +++ b/tests/pageobjects/main-content.page.js @@ -70,6 +70,7 @@ class MainContent extends Page { this.setTextToInput(text); this.sendBtn.click(); browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .body', 5000); return browser.getText('.message:last-child .body') === text; }, 2000); } @@ -105,18 +106,21 @@ class MainContent extends Page { waitForLastMessageTextAttachmentEqualsText(text) { browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .attachment-text', 5000); return browser.getText('.message:last-child .attachment-text') === text; }, 2000); } waitForLastMessageEqualsText(text) { browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .body', 5000); return browser.getText('.message:last-child .body') === text; }, 4000); } waitForLastMessageUserEqualsText(text) { browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .user-card-message:nth-of-type(2)', 5000); return browser.getText('.message:last-child .user-card-message:nth-of-type(2)') === text; }, 2000); } diff --git a/tests/pageobjects/side-nav.page.js b/tests/pageobjects/side-nav.page.js index f83e9a693ea73b8d96a88ad620cdb24a1ab19ed8..a7191a4f4c2beb2fc3711d5eeed506ff997d79f3 100644 --- a/tests/pageobjects/side-nav.page.js +++ b/tests/pageobjects/side-nav.page.js @@ -43,6 +43,7 @@ class SideNav extends Page { browser.click(`.rooms-list > .wrapper > ul [title="${ channelName }"]`); this.messageInput.waitForExist(5000); browser.waitUntil(function() { + browser.waitForVisible('.room-title', 5000); return browser.getText('.room-title') === channelName; }, 5000); } @@ -54,6 +55,7 @@ class SideNav extends Page { browser.waitForVisible(`.room-title=${ channelName }`, 10000); browser.click(`.room-title=${ channelName }`); browser.waitUntil(function() { + browser.waitForVisible('.room-title', 5000); return browser.getText('.room-title') === channelName; }, 5000); }