From 9e47952b8287a5a8bde414b5baf09907c764aa6a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 30 Mar 2017 10:06:57 -0300 Subject: [PATCH 001/280] first test --- mocha.opts | 3 + package.json | 52 ++++--- packages/rocketchat-mentions/.babelrc | 4 + packages/rocketchat-mentions/client.js | 41 +----- packages/rocketchat-mentions/mentions.js | 47 +++++++ packages/rocketchat-mentions/package.js | 4 +- .../rocketchat-mentions/tests/client.tests.js | 131 ++++++++++++++++++ tests/chimp-config.js | 2 +- 8 files changed, 222 insertions(+), 62 deletions(-) create mode 100644 mocha.opts create mode 100644 packages/rocketchat-mentions/.babelrc create mode 100644 packages/rocketchat-mentions/mentions.js create mode 100644 packages/rocketchat-mentions/tests/client.tests.js diff --git a/mocha.opts b/mocha.opts new file mode 100644 index 0000000000..47c929f06e --- /dev/null +++ b/mocha.opts @@ -0,0 +1,3 @@ +--compilers js:babel-core/register +--reporter spec +--ui bdd diff --git a/package.json b/package.json index 8c3773a2ef..6efcd5bf22 100644 --- a/package.json +++ b/package.json @@ -7,27 +7,36 @@ "url": "https://rocket.chat/" }, "contributors": [{ - "name": "Aaron Ogle", - "email": "aaron.ogle@rocket.chat" - }, { - "name": "Bradley Hilton", - "email": "bradley.hilton@rocket.chat" - }, { - "name": "Diego Sampaio", - "email": "diego.sampaio@rocket.chat" - }, { - "name": "Gabriel Engel", - "email": "gabriel.engel@rocket.chat" - }, { - "name": "Marcelo Schmidt", - "email": "marcelo.schmidt@rocket.chat" - }, { - "name": "Rodrigo Nascimento", - "email": "rodrigo.nascimento@rocket.chat" - }, { - "name": "Sing Li", - "email": "sing.li@rocket.chat" + "name": "Aaron Ogle", + "email": "aaron.ogle@rocket.chat" + }, { + "name": "Bradley Hilton", + "email": "bradley.hilton@rocket.chat" + }, { + "name": "Diego Sampaio", + "email": "diego.sampaio@rocket.chat" + }, { + "name": "Gabriel Engel", + "email": "gabriel.engel@rocket.chat" + }, { + "name": "Marcelo Schmidt", + "email": "marcelo.schmidt@rocket.chat" + }, { + "name": "Rodrigo Nascimento", + "email": "rodrigo.nascimento@rocket.chat" + }, { + "name": "Sing Li", + "email": "sing.li@rocket.chat" }], + "mocha": { + "tests": [ + "packages/**/*.tests.js" + ], + "files": [ + "packages/**/*.js", + "!packages/**/*.tests.js" + ] + }, "keywords": [ "rocketchat", "rocket", @@ -42,7 +51,8 @@ "deploy": "npm run build && pm2 startOrRestart pm2.json", "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests/end-to-end", "chimp-test": "chimp tests/chimp-config.js", - "postinstall": "cd packages/rocketchat-katex && npm i" + "postinstall": "cd packages/rocketchat-katex && npm i", + "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"" }, "license": "MIT", "repository": { diff --git a/packages/rocketchat-mentions/.babelrc b/packages/rocketchat-mentions/.babelrc new file mode 100644 index 0000000000..42d3b8ebc2 --- /dev/null +++ b/packages/rocketchat-mentions/.babelrc @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-runtime"], + "presets": ["es2015","es2016", "stage-1", "babel-polyfill"] +} diff --git a/packages/rocketchat-mentions/client.js b/packages/rocketchat-mentions/client.js index 041eb0bfb1..2e7948f5cf 100644 --- a/packages/rocketchat-mentions/client.js +++ b/packages/rocketchat-mentions/client.js @@ -1,41 +1,4 @@ -/* - * Mentions is a named function that will process Mentions - * @param {Object} message - The message object - */ - -function MentionsClient(message) { - let msg = (message && message.html) || ''; - if (!msg.trim()) { - return message; - } - const msgMentionRegex = new RegExp(`(?:^|\\s|\\n)(@(${ RocketChat.settings.get('UTF8_Names_Validation') }):?)[:.,\s]?`, 'g'); - - let me = Meteor.user(); - me = me ? me.username : null; - - msg = msg.replace(msgMentionRegex, function(match, mention, username) { - if (['all', 'here'].includes(username)) { - return match.replace(mention, `${ mention }`); - } - if (message.temp == null && _.findWhere(message.mentions, {username}) == null) { - return match; - } - return match.replace(mention, `${ mention }`); - }); - - const msgChannelRegex = new RegExp(`(?:^|\\s|\\n)(#(${ RocketChat.settings.get('UTF8_Names_Validation') }))[:.,\s]?`, 'g'); - - msg = msg.replace(msgChannelRegex, function(match, mention, name) { - if (message.temp == null && _.findWhere(message.channels, {name}) == null) { - return match; - } - return match.replace(mention, `${ mention }`); - }); - message.html = msg; - return message; -} - - +import mentions from './mentions'; +const {MentionsClient} = mentions(RocketChat); RocketChat.callbacks.add('renderMessage', MentionsClient, RocketChat.callbacks.priority.MEDIUM, 'mentions-message'); - RocketChat.callbacks.add('renderMentions', MentionsClient, RocketChat.callbacks.priority.MEDIUM, 'mentions-mentions'); diff --git a/packages/rocketchat-mentions/mentions.js b/packages/rocketchat-mentions/mentions.js new file mode 100644 index 0000000000..21e8b63d2b --- /dev/null +++ b/packages/rocketchat-mentions/mentions.js @@ -0,0 +1,47 @@ +/* +* Mentions is a named function that will process Mentions +* @param {Object} message - The message object +*/ +import _ from 'underscore'; +export default (RocketChat) => { + const RegExps = { + get msgMentionRegex() { + return new RegExp(`(?:^|\\s|\\n)(@(${ RocketChat.settings.get('UTF8_Names_Validation') }):?)[:.,\s]?`, 'g'); + }, + get msgChannelRegex() { + return new RegExp(`(?:^|\\s|\\n)(#(${ RocketChat.settings.get('UTF8_Names_Validation') }))[:.,\s]?`, 'g'); + } + }; + + function replaceUsers(match, mention, username, message, me) { + if (['all', 'here'].includes(username)) { + return match.replace(mention, `${ mention }`); + } + if (message.temp == null && _.findWhere(message.mentions, {username}) == null) { + return match; + } + return match.replace(mention, `${ mention }`); + } + + function replaceChannels(match, mention, name, message) { + if (message.temp == null && _.findWhere(message.channels, {name}) == null) { + return match; + } + return match.replace(mention, `${ mention }`); + } + + function MentionsClient(message) { + let msg = (message && message.html) || ''; + console.log(123); + if (!msg.trim()) { + return message; + } + let me = Meteor.user(); + me = me ? me.username : null; + msg = msg.replace(RegExps.msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); + msg = msg.replace(RegExps.msgChannelRegex, (match, mention, name) => replaceChannels.apply(this, [match, mention, name, message])); + message.html = msg; + return message; + } + return {MentionsClient, msgMentionRegex: RegExps.msgMentionRegex, msgChannelRegex: RegExps.msgChannelRegex, replaceUsers, replaceChannels}; +}; diff --git a/packages/rocketchat-mentions/package.js b/packages/rocketchat-mentions/package.js index 80be961080..ccc3cf51ab 100644 --- a/packages/rocketchat-mentions/package.js +++ b/packages/rocketchat-mentions/package.js @@ -8,9 +8,11 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'rocketchat:lib' + 'rocketchat:lib', + 'underscore' ]); api.addFiles('server.js', 'server'); api.addFiles('client.js', 'client'); + // api.('mentions.js', 'client'); }); diff --git a/packages/rocketchat-mentions/tests/client.tests.js b/packages/rocketchat-mentions/tests/client.tests.js new file mode 100644 index 0000000000..4c706e9446 --- /dev/null +++ b/packages/rocketchat-mentions/tests/client.tests.js @@ -0,0 +1,131 @@ +/* eslint-env mocha */ +import assert from 'assert'; + +global.RocketChat = typeof global.RocketChat === 'undefined' ? {} : global.RocketChat; + +const confs = { + 'UTF8_Names_Validation' : '[0-9a-zA-Z-_.]+' +}; + +global.RocketChat.settings = { + get(key) { + return confs[key]; + } +}; + +const {msgMentionRegex, msgChannelRegex, replaceUsers, replaceChannels} = require('../mentions').default(RocketChat); + +describe('Testing regex\'s', function() { + describe(`msgMentionRegex: ${ msgMentionRegex }`, function() { + const str1 = '@rocket.cat'; + it(`one user '${ str1 }'`, function functionName() { + const result = str1.match(msgMentionRegex)|| []; + assert.equal(result.length, 1); + }); + + it('two users \'@rocket.cat @all\'', function functionName() { + const result = '@rocket.cat @all'.match(msgMentionRegex)|| []; + assert.equal(result.length, 2); + }); + + const strTest3 = 'hello @rocket.cat'; + it(`text before user '${ strTest3 }'`, function functionName() { + const result = strTest3.match(msgMentionRegex)|| []; + assert.equal(result.length, 1); + }); + + const strTest4 = '@rocket.cat, hello'; + it(`text after user '${ strTest4 }'`, function functionName() { + const result = strTest4.match(msgMentionRegex)|| []; + assert.equal(result.length, 1); + }); + + const strTest5 = '@rocket.cat and @user'; + it(`text between users '${ strTest5 }'`, function functionName() { + const result = strTest5.match(msgMentionRegex)|| []; + assert.equal(result.length, 2); + }); + + }); + + describe(`msgChannelRegex: ${ msgChannelRegex }`, function() { + const str1 = '#general'; + it(`one channel '${ str1 }'`, function functionName() { + const result = str1.match(msgChannelRegex)|| []; + assert.equal(result.length, 1); + }); + const str2 = '#general #private'; + it(`two channels '${ str2 }'`, function functionName() { + const result = str2.match(msgChannelRegex)|| []; + assert.equal(result.length, 2); + }); + const strTest3 = 'hello #general'; + it(`text before channels '${ strTest3 }'`, function functionName() { + const result = strTest3.match(msgChannelRegex)|| []; + assert.equal(result.length, 1); + }); + const strTest4 = '#general, hello'; + it(`text after channel '${ strTest4 }'`, function functionName() { + const result = strTest4.match(msgChannelRegex)|| []; + assert.equal(result.length, 1); + }); + + const strTest5 = '#general and #private'; + it(`text between channels '${ strTest5 }'`, function functionName() { + const result = strTest5.match(msgChannelRegex)|| []; + assert.equal(result.length, 2); + }); + }); + +}); + +describe('Testing replace functions', function() { + const message = { + mentions:[{username:'rocket.cat'}, {username:'admin'}], + channels: [{name: 'general'}] + }; + const me = 'admin'; + describe('function replaceChannels', function() { + const str1 = '#general'; + it(`an public existent channel '${ str1 }'`, function() { + const result = str1.replace(msgChannelRegex, (match, mention, name) => replaceChannels.apply(this, [match, mention, name, message])); + assert.equal(result, `${ str1 }`); + }); + + const str2 = '#private'; + it(`an private or non existent channel '${ str2 }'`, function() { + const result = str2.replace(msgChannelRegex, (match, mention, name) => replaceChannels.apply(this, [match, mention, name, message])); + assert.equal(result, str2); + }); + }); + describe('function replaceUsers', function() { + const str1 = '@unknow.user'; + it(`an unknow user '${ str1 }'`, function() { + const result = str1.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); + assert.equal(result, str1); + }); + const str2 = '@rocket.cat'; + it(`a know user '${ str2 }'`, function() { + const result = str2.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); + assert.equal(result, `${ str2 }`); + }); + + const str3 = '@all'; + it(`'${ str3 }'`, function() { + const result = str3.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); + assert.equal(result, `${ str3 }`); + }); + + const str4 = '@here'; + it(`'${ str4 }'`, function() { + const result = str4.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); + assert.equal(result, `${ str4 }`); + }); + + const str5 = '@admin'; + it(`me '${ str5 }'`, function() { + const result = str5.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); + assert.equal(result, `${ str5 }`); + }); + }); +}); diff --git a/tests/chimp-config.js b/tests/chimp-config.js index cf8445fcc0..4a23f54604 100644 --- a/tests/chimp-config.js +++ b/tests/chimp-config.js @@ -110,7 +110,7 @@ module.exports = { tags: '', grep: null, timeout: 40000, - reporter: 'min', + // reporter: 'min', slow: 100, //retries: 3, bail: false // bail after first test failure -- GitLab From 58399a301315c68a0476fc31a74d74c343b47d81 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 5 Apr 2017 11:20:05 -0300 Subject: [PATCH 002/280] Update chimp-config.js --- tests/chimp-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/chimp-config.js b/tests/chimp-config.js index 4a23f54604..cf8445fcc0 100644 --- a/tests/chimp-config.js +++ b/tests/chimp-config.js @@ -110,7 +110,7 @@ module.exports = { tags: '', grep: null, timeout: 40000, - // reporter: 'min', + reporter: 'min', slow: 100, //retries: 3, bail: false // bail after first test failure -- GitLab From 8305dbd809e1eb640e0082c04e768035b2f55e3e Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 5 Apr 2017 11:23:52 -0300 Subject: [PATCH 003/280] Update package.json --- package.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 6efcd5bf22..19ad536399 100644 --- a/package.json +++ b/package.json @@ -7,26 +7,26 @@ "url": "https://rocket.chat/" }, "contributors": [{ - "name": "Aaron Ogle", - "email": "aaron.ogle@rocket.chat" - }, { - "name": "Bradley Hilton", - "email": "bradley.hilton@rocket.chat" - }, { - "name": "Diego Sampaio", - "email": "diego.sampaio@rocket.chat" - }, { - "name": "Gabriel Engel", - "email": "gabriel.engel@rocket.chat" - }, { - "name": "Marcelo Schmidt", - "email": "marcelo.schmidt@rocket.chat" - }, { - "name": "Rodrigo Nascimento", - "email": "rodrigo.nascimento@rocket.chat" - }, { - "name": "Sing Li", - "email": "sing.li@rocket.chat" + "name": "Aaron Ogle", + "email": "aaron.ogle@rocket.chat" + }, { + "name": "Bradley Hilton", + "email": "bradley.hilton@rocket.chat" + }, { + "name": "Diego Sampaio", + "email": "diego.sampaio@rocket.chat" + }, { + "name": "Gabriel Engel", + "email": "gabriel.engel@rocket.chat" + }, { + "name": "Marcelo Schmidt", + "email": "marcelo.schmidt@rocket.chat" + }, { + "name": "Rodrigo Nascimento", + "email": "rodrigo.nascimento@rocket.chat" + }, { + "name": "Sing Li", + "email": "sing.li@rocket.chat" }], "mocha": { "tests": [ -- GitLab From 4b0deaf891c7e2264116f24209f0372e942f20e0 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 5 Apr 2017 15:23:05 -0300 Subject: [PATCH 004/280] conversion --- .../rocketchat-ui-message/client/message.js | 364 ++++++++++++++ .../client/messageBox.js | 454 ++++++++++++++++++ .../client/popup/messagePopup.js | 285 +++++++++++ .../client/popup/messagePopupConfig.js | 295 ++++++++++++ .../client/popup/messagePopupEmoji.js | 6 + packages/rocketchat-ui-message/package.js | 8 +- 6 files changed, 1408 insertions(+), 4 deletions(-) create mode 100644 packages/rocketchat-ui-message/client/message.js create mode 100644 packages/rocketchat-ui-message/client/messageBox.js create mode 100644 packages/rocketchat-ui-message/client/popup/messagePopup.js create mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupConfig.js create mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js diff --git a/packages/rocketchat-ui-message/client/message.js b/packages/rocketchat-ui-message/client/message.js new file mode 100644 index 0000000000..9129677d94 --- /dev/null +++ b/packages/rocketchat-ui-message/client/message.js @@ -0,0 +1,364 @@ +import moment from 'moment'; + +Template.message.helpers({ + 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') && ((ref = this.u) != null ? ref._id : void 0) !== Meteor.userId() && !RocketChat.MessageTypes.isSystemMessage(this)) { + 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 != null ? subscription.autoTranslate : void 0) !== 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 = ((ref = this.u) != null ? ref._id : void 0) === Meteor.userId(); + if (!(hasPermission || (isEditAllowed && editOwn))) { + return; + } + const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); + if ((blockEditInMinutes != null) && blockEditInMinutes !== 0) { + if (this.ts != null) { + msgTs = moment(this.ts); + } + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockEditInMinutes; + } else { + return true; + } + }, + canDelete() { + let blockDeleteInMinutes, currentTsDiff, deleteOwn, hasPermission, isDeleteAllowed, msgTs, ref; + hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid); + isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); + deleteOwn = ((ref = this.u) != null ? ref._id : void 0) === Meteor.userId(); + if (!(hasPermission || (isDeleteAllowed && deleteOwn))) { + return; + } + blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); + if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0) { + if (this.ts != null) { + msgTs = moment(this.ts); + } + 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() { + let ref, ref1, ref2, ref3; + if (!(((ref = this.urls) != null ? ref.length : void 0) > 0 && (Template.oembedBaseWidget != null) && RocketChat.settings.get('API_Embed'))) { + return false; + } + if (ref1 = (ref2 = this.u) != null ? ref2.username : void 0, indexOf.call((ref3 = RocketChat.settings.get('API_EmbedDisabledFor')) != null ? ref3.split(',').map(function(username) { + return username.trim(); + }) : void 0, ref1) >= 0) { + return false; + } + return true; + }, + reactions() { + let emoji, msgReactions, reaction, ref, total, userUsername, usernames; + msgReactions = []; + userUsername = Meteor.user().username; + ref = this.reactions; + for (emoji in ref) { + reaction = ref[emoji]; + 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] !== '@') { + usernames = usernames[0].toUpperCase() + usernames.substr(1); + } + msgReactions.push({ + emoji, + count: reaction.usernames.length, + usernames, + reaction: ` ${ t('Reacted_with').toLowerCase() } ${ emoji }`, + userReacted: reaction.usernames.indexOf(userUsername) > -1 + }); + } + return msgReactions; + }, + 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() { + let subscription; + subscription = RocketChat.models.Subscriptions.findOne({ + rid: this.rid + }); + if (subscription == null) { + return 'hidden'; + } + }, + hideUsernames() { + let prefs, ref, ref1; + prefs = (ref = Meteor.user()) != null ? (ref1 = ref.settings) != null ? ref1.preferences : void 0 : void 0; + if (prefs != null ? prefs.hideUsernames : void 0) { + + } + } +}); + +Template.message.onCreated(function() { + let msg; + msg = Template.currentData(); + this.wasEdited = (msg.editedAt != null) && !RocketChat.MessageTypes.isSystemMessage(msg); + return this.body = (function() { + let isSystemMessage, messageType, ref; + isSystemMessage = RocketChat.MessageTypes.isSystemMessage(msg); + messageType = RocketChat.MessageTypes.getType(msg); + if ((messageType != null ? messageType.render : void 0) != null) { + msg = messageType.render(msg); + } else if ((messageType != null ? messageType.template : void 0) != null) { + + } else if ((messageType != null ? messageType.message : void 0) != null) { + if ((typeof messageType.data === 'function' ? messageType.data(msg) : void 0) != null) { + msg = TAPi18n.__(messageType.message, messageType.data(msg)); + } else { + msg = TAPi18n.__(messageType.message); + } + } else if (((ref = msg.u) != null ? ref.username : void 0) === RocketChat.settings.get('Chatops_Username')) { + msg.html = msg.msg; + msg = RocketChat.callbacks.run('renderMentions', msg); + msg = msg.html; + } else { + msg = renderMessageBody(msg); + } + if (isSystemMessage) { + return RocketChat.Markdown(msg); + } else { + return msg; + } + }()); +}); + +Template.message.onViewRendered = function(context) { + let view; + view = this; + return this._domrange.onAttached(function(domRange) { + let $currentNode, $nextNode, currentDataset, currentMessageDate, currentNode, newMessage, nextDataset, nextNode, previousDataset, previousMessageDate, previousNode, ref, templateInstance; + currentNode = domRange.lastNode(); + currentDataset = currentNode.dataset; + previousNode = currentNode.previousElementSibling; + nextNode = currentNode.nextElementSibling; + $currentNode = $(currentNode); + $nextNode = $(nextNode); + if (previousNode == null) { + $currentNode.addClass('new-day').removeClass('sequential'); + } else if ((previousNode != null ? previousNode.dataset : void 0) != null) { + previousDataset = previousNode.dataset; + previousMessageDate = new Date(parseInt(previousDataset.timestamp)); + 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 != null ? nextNode.dataset : void 0) != null) { + 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) { + templateInstance = $(`#chat-window-${ context.rid }`)[0] ? (ref = Blaze.getView($(`#chat-window-${ context.rid }`)[0])) != null ? ref.templateInstance() : void 0 : null; + if (currentNode.classList.contains('own') === true) { + return templateInstance != null ? templateInstance.atBottom = true : void 0; + } else if ((templateInstance != null ? templateInstance.firstNode : void 0) && (templateInstance != null ? templateInstance.atBottom : void 0) === false) { + newMessage = templateInstance != null ? templateInstance.find('.new-message') : void 0; + return newMessage != null ? newMessage.className = 'new-message background-primary-action-color color-content-background-color ' : void 0; + } + } + }); +}; diff --git a/packages/rocketchat-ui-message/client/messageBox.js b/packages/rocketchat-ui-message/client/messageBox.js new file mode 100644 index 0000000000..f882b3bc0f --- /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(); + 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 = `
\n \n
`; + 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.js b/packages/rocketchat-ui-message/client/popup/messagePopup.js new file mode 100644 index 0000000000..57a160bc13 --- /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.js b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js new file mode 100644 index 0000000000..797e77fa9c --- /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.js b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js new file mode 100644 index 0000000000..934067a235 --- /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 4d26d91283..881c8687a1 100644 --- a/packages/rocketchat-ui-message/package.js +++ b/packages/rocketchat-ui-message/package.js @@ -34,11 +34,11 @@ Package.onUse(function(api) { 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/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'); -- GitLab From 810c03b7ce5154ea213e01285e2132ed95be3688 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 6 Apr 2017 14:39:56 -0300 Subject: [PATCH 005/280] phase 2 --- package.json | 6 +- packages/rocketchat-importer-slack/main.js | 5 + packages/rocketchat-mentions/Mentions.js | 65 ++++ .../rocketchat-mentions/MentionsServer.js | 78 +++++ packages/rocketchat-mentions/client.js | 16 +- packages/rocketchat-mentions/mentions.js | 47 --- packages/rocketchat-mentions/server.js | 52 +-- .../rocketchat-mentions/tests/client.tests.js | 301 ++++++++++++------ .../rocketchat-mentions/tests/server.tests.js | 46 +++ 9 files changed, 411 insertions(+), 205 deletions(-) create mode 100644 packages/rocketchat-importer-slack/main.js create mode 100644 packages/rocketchat-mentions/Mentions.js create mode 100644 packages/rocketchat-mentions/MentionsServer.js delete mode 100644 packages/rocketchat-mentions/mentions.js create mode 100644 packages/rocketchat-mentions/tests/server.tests.js diff --git a/package.json b/package.json index 6efcd5bf22..7f7c6cc019 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ }, { "name": "Diego Sampaio", "email": "diego.sampaio@rocket.chat" + }, { + "name": "Guilherme Gazzo", + "email": "guilherme.gazzo@rocket.chat" }, { "name": "Gabriel Engel", "email": "gabriel.engel@rocket.chat" @@ -52,7 +55,8 @@ "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests/end-to-end", "chimp-test": "chimp tests/chimp-config.js", "postinstall": "cd packages/rocketchat-katex && npm i", - "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"" + "testunit": "mocha --watch --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", + "coverage": "nyc -r html mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"" }, "license": "MIT", "repository": { diff --git a/packages/rocketchat-importer-slack/main.js b/packages/rocketchat-importer-slack/main.js new file mode 100644 index 0000000000..d7298a2700 --- /dev/null +++ b/packages/rocketchat-importer-slack/main.js @@ -0,0 +1,5 @@ +/* globals Importer */ +Importer.addImporter('slack', Importer.Slack, { + name: 'Slack', + mimeType: 'application/zip' +}); diff --git a/packages/rocketchat-mentions/Mentions.js b/packages/rocketchat-mentions/Mentions.js new file mode 100644 index 0000000000..1608252488 --- /dev/null +++ b/packages/rocketchat-mentions/Mentions.js @@ -0,0 +1,65 @@ +/* +* Mentions is a named function that will process Mentions +* @param {Object} message - The message object +*/ +import _ from 'underscore'; +export default class { + constructor({pattern, me}) { + this.pattern = pattern; + this.me = me; + } + set me(m) { + this._me = m; + } + get me() { + return typeof this._me === 'function' ? this._me() : this._me; + } + set pattern(p) { + this._pattern = p; + } + get pattern() { + return typeof this._pattern === 'function' ? this._pattern() : this._pattern; + } + get userMentionRegex() { + return new RegExp(`@(${ this.pattern })`, 'gm'); + } + get channelMentionRegex() { + return new RegExp(`#(${ this.pattern })`, 'gm'); + } + replaceUsers(str, message, me) { + return str.replace(this.userMentionRegex, (match, username) => { + if (['all', 'here'].includes(username)) { + return `${ match }`; + } + + if (message.temp == null && _.findWhere(message.mentions, {username}) == null) { + return match; + } + return `${ match }`; + }); + } + replaceChannels(str, message) { + return str.replace(this.channelMentionRegex, (match, name) => { + if (message.temp == null && _.findWhere(message.channels, {name}) == null) { + return match; + } + return `${ match }`; + }); + } + getUserMentions(str) { + return str.match(this.userMentionRegex) || []; + } + getChannelMentions(str) { + return str.match(this.channelMentionRegex) || []; + } + parse(message) { + let msg = (message && message.html) || ''; + if (!msg.trim()) { + return message; + } + msg = this.replaceUsers(msg, message, this.me); + msg = this.replaceChannels(msg, message, this.me); + message.html = msg; + return message; + } +} diff --git a/packages/rocketchat-mentions/MentionsServer.js b/packages/rocketchat-mentions/MentionsServer.js new file mode 100644 index 0000000000..8baa163e67 --- /dev/null +++ b/packages/rocketchat-mentions/MentionsServer.js @@ -0,0 +1,78 @@ +/* +* Mentions is a named function that will process Mentions +* @param {Object} message - The message object +*/ +import Mentions from './Mentions'; +export default class MentionsServer extends Mentions { + constructor(args) { + super(args); + this.messageMaxAll = args.messageMaxAll; + this.getChannel = args.getChannel; + this.getUsers = args.getUsers; + } + getChannels(channels) { + return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); + } + set getUsers(m) { + this._getUsers = m; + } + get getUsers() { + return typeof this._getUsers === 'function' ? this._getUsers : () => this._getUsers; + } + set getChannel(m) { + this._getChannel = m; + } + get getChannel() { + return typeof this._getChannel === 'function' ? this._getChannel : () => this._getChannel; + } + set messageMaxAll(m) { + this._messageMaxAll = m; + } + get messageMaxAll() { + return typeof this._messageMaxAll === 'function' ? this._messageMaxAll() : this._messageMaxAll; + } + getUsersByMentions({msg, rid}) { + let mentions = this.getUserMentions(msg); + const mentionsAll = []; + const userMentions = []; + + mentions.forEach((m) => { + const mention = m.trim().substr(1); + if (mention !== 'all' && mention !== 'here') { + return userMentions.push(mention); + } + if (mention === 'all') { + const messageMaxAll = this.messageMaxAll; + const allChannel = this.getChannel(rid); + if (messageMaxAll !== 0 && allChannel.usernames.length >= messageMaxAll) { + return; + } + } + mentionsAll.push({ + _id: mention, + username: mention + }); + }); + mentions = userMentions.length ? this.getUsers(userMentions) : []; + + return [...mentionsAll, ...mentions]; + } + getChannelbyMentions(message) { + let channels = message.msg.match(this.channelMentionRegex); + if (channels) { + channels = channels.map(c => c.trim().substr(1)); + return this.getChannels(channels); + } + } + execute(message) { + const mentionsAll = this.getUsersByMentions(message); + const channels = this.getChannelbyMentions(message); + if (mentionsAll.length !== 0) { + message.mentions = mentionsAll; + } + if (channels.length !== 0) { + message.channels = channels; + } + return message; + } +} diff --git a/packages/rocketchat-mentions/client.js b/packages/rocketchat-mentions/client.js index 2e7948f5cf..a2e1459a65 100644 --- a/packages/rocketchat-mentions/client.js +++ b/packages/rocketchat-mentions/client.js @@ -1,4 +1,12 @@ -import mentions from './mentions'; -const {MentionsClient} = mentions(RocketChat); -RocketChat.callbacks.add('renderMessage', MentionsClient, RocketChat.callbacks.priority.MEDIUM, 'mentions-message'); -RocketChat.callbacks.add('renderMentions', MentionsClient, RocketChat.callbacks.priority.MEDIUM, 'mentions-mentions'); +import Mentions from './mentions'; +const MentionsClient = new Mentions({ + pattern() { + return RocketChat.settings.get('UTF8_Names_Validation'); + }, + me() { + const me = Meteor.user(); + return me && me.username; + } +}); +RocketChat.callbacks.add('renderMessage', (message) => MentionsClient.parse(message), RocketChat.callbacks.priority.MEDIUM, 'mentions-message'); +RocketChat.callbacks.add('renderMentions', (message) => MentionsClient.parse(message), RocketChat.callbacks.priority.MEDIUM, 'mentions-mentions'); diff --git a/packages/rocketchat-mentions/mentions.js b/packages/rocketchat-mentions/mentions.js deleted file mode 100644 index 21e8b63d2b..0000000000 --- a/packages/rocketchat-mentions/mentions.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -* Mentions is a named function that will process Mentions -* @param {Object} message - The message object -*/ -import _ from 'underscore'; -export default (RocketChat) => { - const RegExps = { - get msgMentionRegex() { - return new RegExp(`(?:^|\\s|\\n)(@(${ RocketChat.settings.get('UTF8_Names_Validation') }):?)[:.,\s]?`, 'g'); - }, - get msgChannelRegex() { - return new RegExp(`(?:^|\\s|\\n)(#(${ RocketChat.settings.get('UTF8_Names_Validation') }))[:.,\s]?`, 'g'); - } - }; - - function replaceUsers(match, mention, username, message, me) { - if (['all', 'here'].includes(username)) { - return match.replace(mention, `${ mention }`); - } - if (message.temp == null && _.findWhere(message.mentions, {username}) == null) { - return match; - } - return match.replace(mention, `${ mention }`); - } - - function replaceChannels(match, mention, name, message) { - if (message.temp == null && _.findWhere(message.channels, {name}) == null) { - return match; - } - return match.replace(mention, `${ mention }`); - } - - function MentionsClient(message) { - let msg = (message && message.html) || ''; - console.log(123); - if (!msg.trim()) { - return message; - } - let me = Meteor.user(); - me = me ? me.username : null; - msg = msg.replace(RegExps.msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); - msg = msg.replace(RegExps.msgChannelRegex, (match, mention, name) => replaceChannels.apply(this, [match, mention, name, message])); - message.html = msg; - return message; - } - return {MentionsClient, msgMentionRegex: RegExps.msgMentionRegex, msgChannelRegex: RegExps.msgChannelRegex, replaceUsers, replaceChannels}; -}; diff --git a/packages/rocketchat-mentions/server.js b/packages/rocketchat-mentions/server.js index 66aa59c086..5c2b736cf7 100644 --- a/packages/rocketchat-mentions/server.js +++ b/packages/rocketchat-mentions/server.js @@ -1,49 +1,5 @@ -/* -* Mentions is a named function that will process Mentions -* @param {Object} message - The message object -*/ - -function MentionsServer(message) { - const msgMentionRegex = new RegExp(`(?:^|\\s|\\n)(?:@)(${ RocketChat.settings.get('UTF8_Names_Validation') })`, 'g'); - const mentionsAll = []; - const userMentions = []; - let mentions = message.msg.match(msgMentionRegex); - if (mentions) { - mentions.forEach((m) => { - const mention = m.trim().substr(1); - if (mention !== 'all' && mention !== 'here') { - return userMentions.push(mention); - } - if (mention === 'all') { - const messageMaxAll = RocketChat.settings.get('Message_MaxAll'); - const allChannel = RocketChat.models.Rooms.findOneById(message.rid); - if (messageMaxAll !== 0 && allChannel.usernames.length >= messageMaxAll) { - return; - } - } - mentionsAll.push({ - _id: mention, - username: mention - }); - }); - mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true }}).fetch() : []; - - const verifiedMentions = [...mentionsAll, ...mentions]; - if (verifiedMentions.length !== 0) { - message.mentions = verifiedMentions; - } - } - - const msgChannelRegex = new RegExp(`(?:^|\\s|\\n)(?:#)(${ RocketChat.settings.get('UTF8_Names_Validation') })`, 'g'); - let channels = message.msg.match(msgChannelRegex); - if (channels) { - channels = channels.map(c => c.trim().substr(1)); - const verifiedChannels = RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); - if (verifiedChannels.length !== 0) { - message.channels = verifiedChannels; - } - } - return message; -} - +import MentionsServer from './MentionsServer'; +const mention = new MentionsServer({ + // pattern: +}); RocketChat.callbacks.add('beforeSaveMessage', MentionsServer, RocketChat.callbacks.priority.HIGH, 'mentions'); diff --git a/packages/rocketchat-mentions/tests/client.tests.js b/packages/rocketchat-mentions/tests/client.tests.js index 4c706e9446..e6adff0c06 100644 --- a/packages/rocketchat-mentions/tests/client.tests.js +++ b/packages/rocketchat-mentions/tests/client.tests.js @@ -1,131 +1,222 @@ /* eslint-env mocha */ import assert from 'assert'; -global.RocketChat = typeof global.RocketChat === 'undefined' ? {} : global.RocketChat; - -const confs = { - 'UTF8_Names_Validation' : '[0-9a-zA-Z-_.]+' -}; - -global.RocketChat.settings = { - get(key) { - return confs[key]; - } -}; - -const {msgMentionRegex, msgChannelRegex, replaceUsers, replaceChannels} = require('../mentions').default(RocketChat); - -describe('Testing regex\'s', function() { - describe(`msgMentionRegex: ${ msgMentionRegex }`, function() { - const str1 = '@rocket.cat'; - it(`one user '${ str1 }'`, function functionName() { - const result = str1.match(msgMentionRegex)|| []; - assert.equal(result.length, 1); - }); +import Mentions from '../Mentions'; +const mention = new Mentions({ + pattern: '[0-9a-zA-Z-_.]+', + me: () => 'me' +}); - it('two users \'@rocket.cat @all\'', function functionName() { - const result = '@rocket.cat @all'.match(msgMentionRegex)|| []; - assert.equal(result.length, 2); +describe('Mention', function() { + describe('get pattern', () => { + const regexp = '[0-9a-zA-Z-_.]+'; + describe('by function', function functionName() { + before(() => mention.pattern = () => regexp); + it(`should be equal to ${ regexp }`, ()=> { + assert.equal(regexp, mention.pattern); + }); + }); + describe('by const', function functionName() { + before(() => mention.pattern = regexp); + it(`should be equal to ${ regexp }`, ()=> { + assert.equal(regexp, mention.pattern); + }); }); - - const strTest3 = 'hello @rocket.cat'; - it(`text before user '${ strTest3 }'`, function functionName() { - const result = strTest3.match(msgMentionRegex)|| []; - assert.equal(result.length, 1); + }); + describe('get me', () => { + const me = 'me'; + describe('by function', function functionName() { + before(() => mention.me = () => me); + it(`should be equal to ${ me }`, ()=> { + assert.equal(me, mention.me); + }); + }); + describe('by const', function functionName() { + before(() => mention.me = me); + it(`should be equal to ${ me }`, ()=> { + assert.equal(me, mention.me); + }); }); - - const strTest4 = '@rocket.cat, hello'; - it(`text after user '${ strTest4 }'`, function functionName() { - const result = strTest4.match(msgMentionRegex)|| []; - assert.equal(result.length, 1); + }); + describe('getUserMentions', function functionName() { + describe('for simple text, no mentions', () => { + const result = []; + [ + '#rocket.cat', + 'hello rocket.cat how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getUserMentions(text)); + }); + }); + }); + describe('for one user', () => { + const result = ['@rocket.cat']; + [ + '@rocket.cat', + ' @rocket.cat ', + 'hello @rocket.cat', + 'hello,@rocket.cat', + '@rocket.cat, hello', + '@rocket.cat,hello', + 'hello @rocket.cat how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getUserMentions(text)); + }); + }); + it.skip('shoud return without the "." from "@rocket.cat."', () => { + assert.deepEqual(result, mention.getUserMentions('@rocket.cat.')); + }); + it.skip('shoud return without the "_" from "@rocket.cat_"', () => { + assert.deepEqual(result, mention.getUserMentions('@rocket.cat_')); + }); + it.skip('shoud return without the "-" from "@rocket.cat."', () => { + assert.deepEqual(result, mention.getUserMentions('@rocket.cat-')); + }); + }); + describe('for two users', () => { + const result = ['@rocket.cat', '@all']; + [ + '@rocket.cat @all', + ' @rocket.cat @all ', + 'hello @rocket.cat and @all', + '@rocket.cat, hello @all', + 'hello @rocket.cat and @all how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getUserMentions(text)); + }); + }); }); + }); - const strTest5 = '@rocket.cat and @user'; - it(`text between users '${ strTest5 }'`, function functionName() { - const result = strTest5.match(msgMentionRegex)|| []; - assert.equal(result.length, 2); + describe('getChannelMentions', function functionName() { + describe('for simple text, no mentions', () => { + const result = []; + [ + '@rocket.cat', + 'hello rocket.cat how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getChannelMentions(text)); + }); + }); + }); + describe('for one channel', () => { + const result = ['#general']; + [ + '#general', + ' #general ', + 'hello #general', + '#general, hello', + 'hello #general, how are you?' + ].forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getChannelMentions(text)); + }); + }); + it.skip('shoud return without the "." from "#general."', () => { + assert.deepEqual(result, mention.getUserMentions('#general.')); + }); + it.skip('shoud return without the "_" from "#general_"', () => { + assert.deepEqual(result, mention.getUserMentions('#general_')); + }); + it.skip('shoud return without the "-" from "#general."', () => { + assert.deepEqual(result, mention.getUserMentions('#general-')); + }); + }); + describe('for two channels', () => { + const result = ['#general', '#other']; + [ + '#general #other', + ' #general #other', + 'hello #general and #other', + '#general, hello #other', + 'hello #general #other, how are you?' + ].forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getChannelMentions(text)); + }); + }); }); }); - describe(`msgChannelRegex: ${ msgChannelRegex }`, function() { - const str1 = '#general'; - it(`one channel '${ str1 }'`, function functionName() { - const result = str1.match(msgChannelRegex)|| []; - assert.equal(result.length, 1); - }); - const str2 = '#general #private'; - it(`two channels '${ str2 }'`, function functionName() { - const result = str2.match(msgChannelRegex)|| []; - assert.equal(result.length, 2); - }); - const strTest3 = 'hello #general'; - it(`text before channels '${ strTest3 }'`, function functionName() { - const result = strTest3.match(msgChannelRegex)|| []; - assert.equal(result.length, 1); - }); - const strTest4 = '#general, hello'; - it(`text after channel '${ strTest4 }'`, function functionName() { - const result = strTest4.match(msgChannelRegex)|| []; - assert.equal(result.length, 1); +}); +const message = { + mentions:[{username:'rocket.cat'}, {username:'admin'}, {username: 'me'}], + channels: [{name: 'general'}, {name: 'rocket.cat'}] +}; +describe('replace methods', function() { + describe('replaceUsers', () => { + it('shoud render for @all', () => { + const result = mention.replaceUsers('@all', message, 'me'); + assert.equal('@all', result); + }); + const str2 = '@rocket.cat'; + it(`shoud render for ${ str2 }`, () => { + const result = mention.replaceUsers('@rocket.cat', message, 'me'); + assert.equal(result, `${ str2 }`); }); - const strTest5 = '#general and #private'; - it(`text between channels '${ strTest5 }'`, function functionName() { - const result = strTest5.match(msgChannelRegex)|| []; - assert.equal(result.length, 2); + it(`shoud render for "hello ${ str2 }"`, () => { + const result = mention.replaceUsers(`hello ${ str2 }`, message, 'me'); + assert.equal(result, `hello ${ str2 }`); + }); + it('shoud render for unknow/private user "hello @unknow"', () => { + const result = mention.replaceUsers('hello @unknow', message, 'me'); + assert.equal(result, 'hello @unknow'); + }); + it('shoud render for me', () => { + const result = mention.replaceUsers('hello @me', message, 'me'); + assert.equal(result, 'hello @me'); }); }); - -}); - -describe('Testing replace functions', function() { - const message = { - mentions:[{username:'rocket.cat'}, {username:'admin'}], - channels: [{name: 'general'}] - }; - const me = 'admin'; - describe('function replaceChannels', function() { - const str1 = '#general'; - it(`an public existent channel '${ str1 }'`, function() { - const result = str1.replace(msgChannelRegex, (match, mention, name) => replaceChannels.apply(this, [match, mention, name, message])); - assert.equal(result, `${ str1 }`); + describe('replaceChannels', () => { + it('shoud render for #general', () => { + const result = mention.replaceChannels('#general', message, 'me'); + assert.equal('#general', result); }); - - const str2 = '#private'; - it(`an private or non existent channel '${ str2 }'`, function() { - const result = str2.replace(msgChannelRegex, (match, mention, name) => replaceChannels.apply(this, [match, mention, name, message])); - assert.equal(result, str2); + const str2 = '#rocket.cat'; + it(`shoud render for ${ str2 }`, () => { + const result = mention.replaceChannels(str2, message, 'me'); + assert.equal(result, `${ str2 }`); + }); + it(`shoud render for "hello ${ str2 }"`, () => { + const result = mention.replaceChannels(`hello ${ str2 }`, message, 'me'); + assert.equal(result, `hello ${ str2 }`); + }); + it('shoud render for unknow/private channel "hello #unknow"', () => { + const result = mention.replaceChannels('hello #unknow', message, 'me'); + assert.equal(result, 'hello #unknow'); }); }); - describe('function replaceUsers', function() { - const str1 = '@unknow.user'; - it(`an unknow user '${ str1 }'`, function() { - const result = str1.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); - assert.equal(result, str1); + describe('parse all', () => { + it('shoud render for #general', () => { + message.html = '#general'; + const result = mention.parse(message, 'me'); + assert.equal('#general', result.html); }); - const str2 = '@rocket.cat'; - it(`a know user '${ str2 }'`, function() { - const result = str2.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); - assert.equal(result, `${ str2 }`); + it('shoud render for "#general and @rocket.cat', () => { + message.html = '#general and @rocket.cat'; + const result = mention.parse(message, 'me'); + assert.equal('#general and @rocket.cat', result.html); }); - - const str3 = '@all'; - it(`'${ str3 }'`, function() { - const result = str3.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); - assert.equal(result, `${ str3 }`); + it('shoud render for "', () => { + message.html = ''; + const result = mention.parse(message, 'me'); + assert.equal('', result.html); }); - - const str4 = '@here'; - it(`'${ str4 }'`, function() { - const result = str4.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); - assert.equal(result, `${ str4 }`); + it('shoud render for "simple text', () => { + message.html = 'simple text'; + const result = mention.parse(message, 'me'); + assert.equal('simple text', result.html); }); - const str5 = '@admin'; - it(`me '${ str5 }'`, function() { - const result = str5.replace(msgMentionRegex, (match, mention, username) => replaceUsers.apply(this, [match, mention, username, message, me])); - assert.equal(result, `${ str5 }`); - }); }); }); diff --git a/packages/rocketchat-mentions/tests/server.tests.js b/packages/rocketchat-mentions/tests/server.tests.js new file mode 100644 index 0000000000..c8c4b5e355 --- /dev/null +++ b/packages/rocketchat-mentions/tests/server.tests.js @@ -0,0 +1,46 @@ +/* eslint-env mocha */ +import assert from 'assert'; + +import MentionsServer from '../MentionsServer'; + + +const mention = new MentionsServer({ + pattern: '[0-9a-zA-Z-_.]+', + messageMaxAll: () => 2, //|| RocketChat.settings.get('Message_MaxAll') + getUsers: (usernames) => { + return Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }, + getChannel: (rid) => { + return { + usernames: [1] + }; + }// RocketChat.models.Rooms.findOneById(message.rid); +}); + +describe('Mention Server', () => { + describe('getUsersByMentions', () => { + + it('should return "all"', () => { + const message = { + msg: '@all' + }; + const expected = [{ + _id: 'all', + username: 'all' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "here"', () => { + const message = { + msg: '@here' + }; + const expected = [{ + _id: 'here', + username: 'here' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); +}); -- GitLab From 2cd075133493159d7e546cc509c75bb694bb23cb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 10 Apr 2017 11:44:22 -0300 Subject: [PATCH 006/280] ajust pattern --- packages/rocketchat-mentions/Mentions.js | 1 + .../rocketchat-mentions/MentionsServer.js | 30 +- packages/rocketchat-mentions/client.js | 2 +- packages/rocketchat-mentions/server.js | 8 +- .../rocketchat-mentions/tests/server.tests.js | 280 +++++++++++++++++- 5 files changed, 286 insertions(+), 35 deletions(-) diff --git a/packages/rocketchat-mentions/Mentions.js b/packages/rocketchat-mentions/Mentions.js index 1608252488..0f4f2dada9 100644 --- a/packages/rocketchat-mentions/Mentions.js +++ b/packages/rocketchat-mentions/Mentions.js @@ -27,6 +27,7 @@ export default class { return new RegExp(`#(${ this.pattern })`, 'gm'); } replaceUsers(str, message, me) { + console.log(message.mentions); return str.replace(this.userMentionRegex, (match, username) => { if (['all', 'here'].includes(username)) { return `${ match }`; diff --git a/packages/rocketchat-mentions/MentionsServer.js b/packages/rocketchat-mentions/MentionsServer.js index 8baa163e67..40132d9432 100644 --- a/packages/rocketchat-mentions/MentionsServer.js +++ b/packages/rocketchat-mentions/MentionsServer.js @@ -8,17 +8,21 @@ export default class MentionsServer extends Mentions { super(args); this.messageMaxAll = args.messageMaxAll; this.getChannel = args.getChannel; + this.getChannels = args.getChannels; this.getUsers = args.getUsers; } - getChannels(channels) { - return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); - } set getUsers(m) { this._getUsers = m; } get getUsers() { return typeof this._getUsers === 'function' ? this._getUsers : () => this._getUsers; } + set getChannels(m) { + this._getChannels = m; + } + get getChannels() { + return typeof this._getChannels === 'function' ? this._getChannels : () => this._getChannels; + } set getChannel(m) { this._getChannel = m; } @@ -54,25 +58,19 @@ export default class MentionsServer extends Mentions { }); }); mentions = userMentions.length ? this.getUsers(userMentions) : []; - return [...mentionsAll, ...mentions]; } - getChannelbyMentions(message) { - let channels = message.msg.match(this.channelMentionRegex); - if (channels) { - channels = channels.map(c => c.trim().substr(1)); - return this.getChannels(channels); - } + getChannelbyMentions({msg}) { + const channels = this.getChannelMentions(msg); + return this.getChannels(channels.map(c => c.trim().substr(1))); } execute(message) { const mentionsAll = this.getUsersByMentions(message); const channels = this.getChannelbyMentions(message); - if (mentionsAll.length !== 0) { - message.mentions = mentionsAll; - } - if (channels.length !== 0) { - message.channels = channels; - } + + message.mentions = mentionsAll; + + message.channels = channels; return message; } } diff --git a/packages/rocketchat-mentions/client.js b/packages/rocketchat-mentions/client.js index a2e1459a65..3bab2bd276 100644 --- a/packages/rocketchat-mentions/client.js +++ b/packages/rocketchat-mentions/client.js @@ -1,4 +1,4 @@ -import Mentions from './mentions'; +import Mentions from './Mentions'; const MentionsClient = new Mentions({ pattern() { return RocketChat.settings.get('UTF8_Names_Validation'); diff --git a/packages/rocketchat-mentions/server.js b/packages/rocketchat-mentions/server.js index 5c2b736cf7..f04030f231 100644 --- a/packages/rocketchat-mentions/server.js +++ b/packages/rocketchat-mentions/server.js @@ -1,5 +1,9 @@ import MentionsServer from './MentionsServer'; const mention = new MentionsServer({ - // pattern: + pattern: () => RocketChat.settings.get('UTF8_Names_Validation'), + messageMaxAll: () => RocketChat.settings.get('Message_MaxAll'), + getUsers: (usernames) => Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(), + getChannel: (rid) => RocketChat.models.Rooms.findOneById(rid), + getChannels: (channels) => RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch() }); -RocketChat.callbacks.add('beforeSaveMessage', MentionsServer, RocketChat.callbacks.priority.HIGH, 'mentions'); +RocketChat.callbacks.add('beforeSaveMessage', (message) => mention.execute(message), RocketChat.callbacks.priority.HIGH, 'mentions'); diff --git a/packages/rocketchat-mentions/tests/server.tests.js b/packages/rocketchat-mentions/tests/server.tests.js index c8c4b5e355..92c0417e33 100644 --- a/packages/rocketchat-mentions/tests/server.tests.js +++ b/packages/rocketchat-mentions/tests/server.tests.js @@ -6,41 +6,289 @@ import MentionsServer from '../MentionsServer'; const mention = new MentionsServer({ pattern: '[0-9a-zA-Z-_.]+', - messageMaxAll: () => 2, //|| RocketChat.settings.get('Message_MaxAll') + messageMaxAll: () => 4, //|| RocketChat.settings.get('Message_MaxAll') getUsers: (usernames) => { - return Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + return [{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }].filter(user => usernames.includes(user.username));//Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); }, getChannel: (rid) => { return { - usernames: [1] + usernames: [{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }] }; - }// RocketChat.models.Rooms.findOneById(message.rid); + // RocketChat.models.Rooms.findOneById(message.rid);, + }, + getChannels(channels) { + return [{ + _id: 1, + name: 'general' + }].filter(channel => channels.includes(channel.name)); + // return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); + } }); describe('Mention Server', () => { describe('getUsersByMentions', () => { + describe('for @all but the number of users is greater than messageMaxAll', () => { + before(() => { + mention.getChannel = () => { + return { + usernames:[{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }, { + _id: 3, + username: 'jon1' + }, { + _id: 4, + username: 'jon2' + }, { + _id: 5, + username: 'jon3' + }] + }; + //Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }; + }); + it('should return nothing', () => { + const message = { + msg: '@all' + }; + const expected = []; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + describe('for one user', () => { + before(() => { + mention.getChannel = () => { + return { + usernames:[{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }] + }; + //Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }; + }); + it('should return "all"', () => { + const message = { + msg: '@all' + }; + const expected = [{ + _id: 'all', + username: 'all' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "here"', () => { + const message = { + msg: '@here' + }; + const expected = [{ + _id: 'here', + username: 'here' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "rocket.cat"', () => { + const message = { + msg: '@rocket.cat' + }; + const expected = [{ + _id: 1, + username: 'rocket.cat' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + describe('for two user', () => { + it('should return "all and here"', () => { + const message = { + msg: '@all @here' + }; + const expected = [{ + _id: 'all', + username: 'all' + }, { + _id: 'here', + username: 'here' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "here and rocket.cat"', () => { + const message = { + msg: '@here @rocket.cat' + }; + const expected = [{ + _id: 'here', + username: 'here' + }, { + _id: 1, + username: 'rocket.cat' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); - it('should return "all"', () => { + it('should return "here, rocket.cat, jon"', () => { + const message = { + msg: '@here @rocket.cat @jon' + }; + const expected = [{ + _id: 'here', + username: 'here' + }, { + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + + describe('for an unknow user', () => { + it('should return "nothing"', () => { + const message = { + msg: '@unknow' + }; + const expected = []; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + + }); + describe('getChannelbyMentions', () => { + it('should return the channel "general"', () => { const message = { - msg: '@all' + msg: '#general' }; const expected = [{ - _id: 'all', - username: 'all' + _id: 1, + name: 'general' }]; - const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + const result = mention.getChannelbyMentions(message); + assert.deepEqual(result, expected); + }); + it('should return nothing"', () => { + const message = { + msg: '#unknow' + }; + const expected = []; + const result = mention.getChannelbyMentions(message); + assert.deepEqual(result, expected); }); - it('should return "here"', () => { + }); + describe('execute', () => { + it('should return the channel "general"', () => { const message = { - msg: '@here' + msg: '#general' }; const expected = [{ - _id: 'here', - username: 'here' + _id: 1, + name: 'general' }]; - const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + const result = mention.getChannelbyMentions(message); + assert.deepEqual(result, expected); + }); + it('should return nothing"', () => { + const message = { + msg: '#unknow' + }; + const expected = { + msg: '#unknow', + mentions: [], + channels: [] + }; + const result = mention.execute(message); + assert.deepEqual(result, expected); + }); + }); + + describe('getters and setters', ()=> { + describe('messageMaxAll', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.messageMaxAll = 4; + assert.deepEqual(mention.messageMaxAll, 4); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.messageMaxAll = () => 4; + assert.deepEqual(mention.messageMaxAll, 4); + }); + }); + }); + describe('getUsers', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.getUsers = 4; + assert.deepEqual(mention.getUsers(), 4); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.getUsers = () => 4; + assert.deepEqual(mention.getUsers(), 4); + }); + }); + }); + describe('getChannels', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.getChannels = 4; + assert.deepEqual(mention.getChannels(), 4); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.getChannels = () => 4; + assert.deepEqual(mention.getChannels(), 4); + }); + }); + }); + describe('getChannel', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.getChannel = true; + assert.deepEqual(mention.getChannel(), true); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.getChannel = () => true; + assert.deepEqual(mention.getChannel(), true); + }); + }); }); }); }); -- GitLab From 9f5fa8d483fcc6fcbbed8dd476835eb97913b93a Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Fri, 14 Apr 2017 00:30:54 +0100 Subject: [PATCH 007/280] Show full name in mentions --- packages/rocketchat-mentions/client.js | 6 ++++-- packages/rocketchat-mentions/server.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rocketchat-mentions/client.js b/packages/rocketchat-mentions/client.js index 041eb0bfb1..81448e3c10 100644 --- a/packages/rocketchat-mentions/client.js +++ b/packages/rocketchat-mentions/client.js @@ -17,10 +17,12 @@ function MentionsClient(message) { if (['all', 'here'].includes(username)) { return match.replace(mention, `${ mention }`); } - if (message.temp == null && _.findWhere(message.mentions, {username}) == null) { + const mentionObj = _.findWhere(message.mentions, {username}); + if (message.temp == null && mentionObj == null) { return match; } - return match.replace(mention, `${ mention }`); + const name = RocketChat.settings.get('UI_Use_Real_Name') && mentionObj && mentionObj.name; + return match.replace(mention, `${ name || mention }`); }); const msgChannelRegex = new RegExp(`(?:^|\\s|\\n)(#(${ RocketChat.settings.get('UTF8_Names_Validation') }))[:.,\s]?`, 'g'); diff --git a/packages/rocketchat-mentions/server.js b/packages/rocketchat-mentions/server.js index 66aa59c086..06877fe6cb 100644 --- a/packages/rocketchat-mentions/server.js +++ b/packages/rocketchat-mentions/server.js @@ -26,7 +26,7 @@ function MentionsServer(message) { username: mention }); }); - mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true }}).fetch() : []; + mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true, name: true }}).fetch() : []; const verifiedMentions = [...mentionsAll, ...mentions]; if (verifiedMentions.length !== 0) { -- GitLab From 9753c716a59f998e4639d5bdab65e89d9c4d258a Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Mon, 17 Apr 2017 22:58:29 +0100 Subject: [PATCH 008/280] Use cache for mention real names --- .../rocketchat-lib/server/methods/getChannelHistory.js | 6 ++++++ packages/rocketchat-mentions/server.js | 2 +- server/methods/loadHistory.js | 6 ++++++ server/stream/messages.js | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/rocketchat-lib/server/methods/getChannelHistory.js b/packages/rocketchat-lib/server/methods/getChannelHistory.js index e2f40ebe74..0f3194991f 100644 --- a/packages/rocketchat-lib/server/methods/getChannelHistory.js +++ b/packages/rocketchat-lib/server/methods/getChannelHistory.js @@ -57,6 +57,12 @@ Meteor.methods({ const user = RocketChat.models.Users.findOneById(message.u._id); message.u.name = user && user.name; } + if (message.mentions && message.mentions.length && UI_Use_Real_Name) { + message.mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + mention.name = user && user.name; + }); + } return message; }); diff --git a/packages/rocketchat-mentions/server.js b/packages/rocketchat-mentions/server.js index 06877fe6cb..92b38dd1d6 100644 --- a/packages/rocketchat-mentions/server.js +++ b/packages/rocketchat-mentions/server.js @@ -26,7 +26,7 @@ function MentionsServer(message) { username: mention }); }); - mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true, name: true }}).fetch() : []; + mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true}}).fetch() : []; const verifiedMentions = [...mentionsAll, ...mentions]; if (verifiedMentions.length !== 0) { diff --git a/server/methods/loadHistory.js b/server/methods/loadHistory.js index 8404d9e81b..963eb70581 100644 --- a/server/methods/loadHistory.js +++ b/server/methods/loadHistory.js @@ -68,6 +68,12 @@ Meteor.methods({ const user = RocketChat.models.Users.findOneById(message.u._id); message.u.name = user && user.name; } + if (message.mentions && message.mentions.length && UI_Use_Real_Name) { + message.mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + mention.name = user && user.name; + }); + } return message; }); diff --git a/server/stream/messages.js b/server/stream/messages.js index cffeb442a7..96afa00f6b 100644 --- a/server/stream/messages.js +++ b/server/stream/messages.js @@ -51,6 +51,13 @@ Meteor.startup(function() { const user = RocketChat.models.Users.findOneById(record.u._id); record.u.name = user && user.name; } + + if (record.mentions && record.mentions.length && UI_Use_Real_Name) { + record.mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + mention.name = user && user.name; + }); + } msgStream.emitWithoutBroadcast('__my_messages__', record, {}); return msgStream.emitWithoutBroadcast(record.rid, record); } -- GitLab From d7a2e5cfc1d523da43de7f400ce97e14a0ffbd05 Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Mon, 17 Apr 2017 23:03:08 +0100 Subject: [PATCH 009/280] Revert rocketchat-mentions/server.js --- packages/rocketchat-mentions/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-mentions/server.js b/packages/rocketchat-mentions/server.js index 92b38dd1d6..66aa59c086 100644 --- a/packages/rocketchat-mentions/server.js +++ b/packages/rocketchat-mentions/server.js @@ -26,7 +26,7 @@ function MentionsServer(message) { username: mention }); }); - mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true}}).fetch() : []; + mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true }}).fetch() : []; const verifiedMentions = [...mentionsAll, ...mentions]; if (verifiedMentions.length !== 0) { -- GitLab From 652e7ac2018e984ab64f4025065bf2f82a9045ce Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Tue, 18 Apr 2017 13:54:48 +0100 Subject: [PATCH 010/280] Update mention name when user changes name --- client/notifications/UsersNameChanged.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/notifications/UsersNameChanged.js b/client/notifications/UsersNameChanged.js index 77d153ba70..68f23ea3e1 100644 --- a/client/notifications/UsersNameChanged.js +++ b/client/notifications/UsersNameChanged.js @@ -10,6 +10,18 @@ Meteor.startup(function() { multi: true }); + RocketChat.models.Messages.update({ + mentions: { + $elemMatch: { _id } + } + }, { + $set: { + 'mentions.$.name': name + } + }, { + multi: true + }); + RocketChat.models.Subscriptions.update({ name: username, t: 'd' -- GitLab From 8930ea911d6546d19bc5987b91cfb0048c2276e0 Mon Sep 17 00:00:00 2001 From: Alex Brazier Date: Tue, 18 Apr 2017 15:35:17 +0100 Subject: [PATCH 011/280] Replace @username with real name in notifications --- .../server/lib/sendNotificationsOnMessage.js | 144 +++++++++++------- 1 file changed, 86 insertions(+), 58 deletions(-) diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js index 85ab61fb11..eb1aa21a28 100644 --- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js @@ -1,6 +1,81 @@ /* globals Push */ import moment from 'moment'; +/** + * Replaces @username with full name + * + * @param {string} message The message to replace + * @param {object[]} mentions Array of mentions used to make replacements + * + * @returns {string} + */ +function replaceMentionedUsernamesWithFullNames(message, mentions) { + if (!mentions || !mentions.length) { + return message; + } + mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + if (user && user.name) { + message = message.replace(`@${ mention.username }`, user.name); + } + }); + return message; +} + +/** + * Send notification to user + * + * @param {string} userId The user to notify + * @param {object} user The sender + * @param {object} room The room send from + * @param {number} duration Duration of notification + */ +function notifyUser(userId, user, message, room, duration) { + const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true; + if (UI_Use_Real_Name) { + message.msg = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions); + } + let title = UI_Use_Real_Name ? user.name : `@${ user.username }`; + if (room.t !== 'd' && room.name) { + title += ` @ #${ room.name }`; + } + RocketChat.Notifications.notifyUser(userId, 'notification', { + title, + text: message.msg, + duration, + payload: { + _id: message._id, + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + } + }); +} + +/** + * Checks if a message contains a user highlight + * + * @param {string} message + * @param {array|undefined} highlights + * + * @returns {boolean} + */ +function messageContainsHighlight(message, highlights) { + if (! highlights || highlights.length === 0) { return false; } + + let has = false; + highlights.some(function(highlight) { + const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); + if (regexp.test(message.msg)) { + has = true; + return true; + } + }); + + return has; +} + RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // skips this callback if the message was edited if (message.editedAt) { @@ -16,7 +91,13 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { /* Increment unread couter if direct messages */ - const settings = {}; + const settings = { + alwaysNotifyDesktopUsers: [], + dontNotifyDesktopUsers: [], + alwaysNotifyMobileUsers: [], + dontNotifyMobileUsers: [], + desktopNotificationDurations: {} + }; /** * Checks if a given user can be notified @@ -35,35 +116,6 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { return (settings[types[type][0]].indexOf(id) === -1 || settings[types[type][1]].indexOf(id) !== -1); } - /** - * Checks if a message contains a user highlight - * - * @param {string} message - * @param {array|undefined} highlights - * - * @returns {boolean} - */ - function messageContainsHighlight(message, highlights) { - if (! highlights || highlights.length === 0) { return false; } - - let has = false; - highlights.some(function(highlight) { - const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); - if (regexp.test(message.msg)) { - has = true; - return true; - } - }); - - return has; - } - - settings.alwaysNotifyDesktopUsers = []; - settings.dontNotifyDesktopUsers = []; - settings.alwaysNotifyMobileUsers = []; - settings.dontNotifyMobileUsers = []; - settings.desktopNotificationDurations = {}; - const notificationPreferencesByRoom = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom(room._id); notificationPreferencesByRoom.forEach(function(subscription) { if (subscription.desktopNotifications === 'all') { @@ -127,18 +179,8 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { } if ((userOfMention != null) && canBeNotified(userOfMentionId, 'mobile')) { - RocketChat.Notifications.notifyUser(userOfMention._id, 'notification', { - title: RocketChat.settings.get('UI_Use_Real_Name') ? user.name : `@${ user.username }`, - text: message.msg, - duration: settings.desktopNotificationDurations[userOfMention._id], - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - } - }); + const duration = settings.desktopNotificationDurations[userOfMention._id]; + notifyUser(userOfMention._id, user, message, room, duration); } if ((userOfMention != null) && canBeNotified(userOfMentionId, 'desktop')) { @@ -273,22 +315,8 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { if (userIdsToNotify.length > 0) { for (const usersOfMentionId of userIdsToNotify) { - let title = `@${ user.username }`; - if (room.name) { - title += ` @ #${ room.name }`; - } - RocketChat.Notifications.notifyUser(usersOfMentionId, 'notification', { - title, - text: message.msg, - duration: settings.desktopNotificationDurations[usersOfMentionId], - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - } - }); + const duration = settings.desktopNotificationDurations[usersOfMentionId]; + notifyUser(usersOfMentionId, user, message, room, duration); } } -- GitLab From 1c2e35bd936d594aaa0a00b3decd76fc8908ed6e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 19 Apr 2017 14:02:52 -0300 Subject: [PATCH 012/280] finished conversion rocketchat-lib --- packages/rocketchat-lib/package.js | 25 +- .../server/functions/Notifications.coffee | 119 --- .../server/functions/Notifications.js | 132 +++ .../functions/checkUsernameAvailability.js | 20 + .../server/functions/sendMessage.coffee | 50 -- .../server/functions/sendMessage.js | 59 ++ .../server/functions/setUsername.coffee | 77 -- .../server/functions/setUsername.js | 85 ++ .../server/functions/settings.coffee | 248 ------ .../server/functions/settings.js | 283 +++++++ .../server/methods/sendMessage.coffee | 64 -- .../server/methods/sendMessage.js | 81 ++ .../rocketchat-lib/server/models/Messages.js | 580 +++++++++++++ .../rocketchat-lib/server/models/Rooms.js | 760 ++++++++++++++++++ .../rocketchat-lib/server/models/Settings.js | 178 ++++ .../server/models/Subscriptions.js | 570 +++++++++++++ .../rocketchat-lib/server/models/Uploads.js | 91 +++ .../rocketchat-lib/server/models/Users.js | 538 +++++++++++++ 18 files changed, 3389 insertions(+), 571 deletions(-) delete mode 100644 packages/rocketchat-lib/server/functions/Notifications.coffee create mode 100644 packages/rocketchat-lib/server/functions/Notifications.js create mode 100644 packages/rocketchat-lib/server/functions/checkUsernameAvailability.js delete mode 100644 packages/rocketchat-lib/server/functions/sendMessage.coffee create mode 100644 packages/rocketchat-lib/server/functions/sendMessage.js delete mode 100644 packages/rocketchat-lib/server/functions/setUsername.coffee create mode 100644 packages/rocketchat-lib/server/functions/setUsername.js delete mode 100644 packages/rocketchat-lib/server/functions/settings.coffee create mode 100644 packages/rocketchat-lib/server/functions/settings.js delete mode 100644 packages/rocketchat-lib/server/methods/sendMessage.coffee create mode 100644 packages/rocketchat-lib/server/methods/sendMessage.js create mode 100644 packages/rocketchat-lib/server/models/Messages.js create mode 100644 packages/rocketchat-lib/server/models/Rooms.js create mode 100644 packages/rocketchat-lib/server/models/Settings.js create mode 100644 packages/rocketchat-lib/server/models/Subscriptions.js create mode 100644 packages/rocketchat-lib/server/models/Uploads.js create mode 100644 packages/rocketchat-lib/server/models/Users.js diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 14a27f270f..b3bccca8c1 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -21,7 +21,6 @@ Package.onUse(function(api) { api.use('reactive-var'); api.use('reactive-dict'); api.use('accounts-base'); - api.use('coffeescript'); api.use('ecmascript'); api.use('random'); api.use('check'); @@ -72,7 +71,7 @@ Package.onUse(function(api) { api.addFiles('server/functions/addUserToDefaultChannels.js', 'server'); api.addFiles('server/functions/addUserToRoom.js', 'server'); api.addFiles('server/functions/archiveRoom.js', 'server'); - api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server'); + api.addFiles('server/functions/checkUsernameAvailability.js', 'server'); api.addFiles('server/functions/checkEmailAvailability.js', 'server'); api.addFiles('server/functions/createRoom.js', 'server'); api.addFiles('server/functions/deleteMessage.js', 'server'); @@ -82,15 +81,15 @@ Package.onUse(function(api) { api.addFiles('server/functions/removeUserFromRoom.js', 'server'); api.addFiles('server/functions/saveUser.js', 'server'); api.addFiles('server/functions/saveCustomFields.js', 'server'); - api.addFiles('server/functions/sendMessage.coffee', 'server'); - api.addFiles('server/functions/settings.coffee', 'server'); + api.addFiles('server/functions/sendMessage.js', 'server'); + api.addFiles('server/functions/settings.js', 'server'); api.addFiles('server/functions/setUserAvatar.js', 'server'); - api.addFiles('server/functions/setUsername.coffee', 'server'); + api.addFiles('server/functions/setUsername.js', 'server'); api.addFiles('server/functions/setRealName.js', 'server'); api.addFiles('server/functions/setEmail.js', 'server'); api.addFiles('server/functions/unarchiveRoom.js', 'server'); api.addFiles('server/functions/updateMessage.js', 'server'); - api.addFiles('server/functions/Notifications.coffee', 'server'); + api.addFiles('server/functions/Notifications.js', 'server'); // SERVER LIB api.addFiles('server/lib/configLogger.js', 'server'); @@ -104,13 +103,13 @@ Package.onUse(function(api) { // SERVER MODELS api.addFiles('server/models/_Base.js', 'server'); - api.addFiles('server/models/Messages.coffee', 'server'); + api.addFiles('server/models/Messages.js', 'server'); api.addFiles('server/models/Reports.js', 'server'); - api.addFiles('server/models/Rooms.coffee', 'server'); - api.addFiles('server/models/Settings.coffee', 'server'); - api.addFiles('server/models/Subscriptions.coffee', 'server'); - api.addFiles('server/models/Uploads.coffee', 'server'); - api.addFiles('server/models/Users.coffee', 'server'); + api.addFiles('server/models/Rooms.js', 'server'); + api.addFiles('server/models/Settings.js', 'server'); + api.addFiles('server/models/Subscriptions.js', 'server'); + api.addFiles('server/models/Uploads.js', 'server'); + api.addFiles('server/models/Users.js', 'server'); api.addFiles('server/oauth/oauth.js', 'server'); api.addFiles('server/oauth/google.js', 'server'); @@ -153,7 +152,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/robotMethods.js', 'server'); api.addFiles('server/methods/saveSetting.js', 'server'); api.addFiles('server/methods/sendInvitationEmail.js', 'server'); - api.addFiles('server/methods/sendMessage.coffee', 'server'); + api.addFiles('server/methods/sendMessage.js', 'server'); api.addFiles('server/methods/sendSMTPTestEmail.js', 'server'); api.addFiles('server/methods/setAdminStatus.js', 'server'); api.addFiles('server/methods/setRealName.js', 'server'); diff --git a/packages/rocketchat-lib/server/functions/Notifications.coffee b/packages/rocketchat-lib/server/functions/Notifications.coffee deleted file mode 100644 index 9e08663db6..0000000000 --- a/packages/rocketchat-lib/server/functions/Notifications.coffee +++ /dev/null @@ -1,119 +0,0 @@ -RocketChat.Notifications = new class - constructor: -> - self = @ - - @debug = false - - @streamAll = new Meteor.Streamer 'notify-all' - @streamLogged = new Meteor.Streamer 'notify-logged' - @streamRoom = new Meteor.Streamer 'notify-room' - @streamRoomUsers = new Meteor.Streamer 'notify-room-users' - @streamUser = new Meteor.Streamer 'notify-user' - - - @streamAll.allowWrite('none') - @streamLogged.allowWrite('none') - @streamRoom.allowWrite('none') - @streamRoomUsers.allowWrite (eventName, args...) -> - [roomId, e] = eventName.split('/') - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - if RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, @userId)? - subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, @userId).fetch() - for subscription in subscriptions - RocketChat.Notifications.notifyUser(subscription.u._id, e, args...) - - return false - - @streamUser.allowWrite('logged') - - @streamAll.allowRead('all') - - @streamLogged.allowRead('logged') - - @streamRoom.allowRead (eventName) -> - if not @userId? then return false - - roomId = eventName.split('/')[0] - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - room = RocketChat.models.Rooms.findOneById(roomId) - if room.t is 'l' and room.v._id is user._id - return true - - return room.usernames.indexOf(user.username) > -1 - - @streamRoomUsers.allowRead('none'); - - @streamUser.allowRead (eventName) -> - userId = eventName.split('/')[0] - return @userId? and @userId is userId - - - notifyAll: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emit.apply @streamAll, args - - notifyLogged: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emit.apply @streamLogged, args - - notifyRoom: (room, eventName, args...) -> - console.log 'notifyRoom', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emit.apply @streamRoom, args - - notifyUser: (userId, eventName, args...) -> - console.log 'notifyUser', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emit.apply @streamUser, args - - - notifyAllInThisInstance: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emitWithoutBroadcast.apply @streamAll, args - - notifyLoggedInThisInstance: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emitWithoutBroadcast.apply @streamLogged, args - - notifyRoomInThisInstance: (room, eventName, args...) -> - console.log 'notifyRoomAndBroadcast', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emitWithoutBroadcast.apply @streamRoom, args - - notifyUserInThisInstance: (userId, eventName, args...) -> - console.log 'notifyUserAndBroadcast', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emitWithoutBroadcast.apply @streamUser, args - - -## Permissions for client - -# Enable emit for event typing for rooms and add username to event data -func = (eventName, username) -> - [room, e] = eventName.split('/') - - if e is 'webrtc' - return true - - if e is 'typing' - user = Meteor.users.findOne(@userId, {fields: {username: 1}}) - if user?.username is username - return true - - return false - -RocketChat.Notifications.streamRoom.allowWrite func diff --git a/packages/rocketchat-lib/server/functions/Notifications.js b/packages/rocketchat-lib/server/functions/Notifications.js new file mode 100644 index 0000000000..89fec97a27 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/Notifications.js @@ -0,0 +1,132 @@ +RocketChat.Notifications = new class { + constructor() { + this.debug = false; + this.streamAll = new Meteor.Streamer('notify-all'); + this.streamLogged = new Meteor.Streamer('notify-logged'); + this.streamRoom = new Meteor.Streamer('notify-room'); + this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); + this.streamUser = new Meteor.Streamer('notify-user'); + this.streamAll.allowWrite('none'); + this.streamLogged.allowWrite('none'); + this.streamRoom.allowWrite('none'); + this.streamRoomUsers.allowWrite(function(eventName, ...args) { + const [roomId, e] = eventName.split('/'); + // const user = Meteor.users.findOne(this.userId, { + // fields: { + // username: 1 + // } + // }); + if (RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) { + const subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch(); + subscriptions.forEach(subscription => RocketChat.Notifications.notifyUser(subscription.u._id, e, ...args)); + } + return false; + }); + this.streamUser.allowWrite('logged'); + this.streamAll.allowRead('all'); + this.streamLogged.allowRead('logged'); + this.streamRoom.allowRead(function(eventName) { + if (this.userId == null) { + return false; + } + const [roomId] = eventName.split('/'); + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + const room = RocketChat.models.Rooms.findOneById(roomId); + if (room.t === 'l' && room.v._id === user._id) { + return true; + } + return room.usernames.indexOf(user.username) > -1; + }); + this.streamRoomUsers.allowRead('none'); + this.streamUser.allowRead(function(eventName) { + const [userId] = eventName.split('/'); + return (this.userId != null) && this.userId === userId; + }); + } + + notifyAll(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emit.apply(this.streamAll, args); + } + + notifyLogged(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emit.apply(this.streamLogged, args); + } + + notifyRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emit.apply(this.streamRoom, args); + } + + notifyUser(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUser', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emit.apply(this.streamUser, args); + } + + notifyAllInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args); + } + + notifyLoggedInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args); + } + + notifyRoomInThisInstance(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoomAndBroadcast', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args); + } + + notifyUserInThisInstance(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUserAndBroadcast', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args); + } +}; + +RocketChat.Notifications.streamRoom.allowWrite(function(eventName, username) { + const [, e] = eventName.split('/'); + if (e === 'webrtc') { + return true; + } + if (e === 'typing') { + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + if (user != null && user.username === username) { + return true; + } + } + return false; +}); diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js new file mode 100644 index 0000000000..01e8f8c1b1 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js @@ -0,0 +1,20 @@ +RocketChat.checkUsernameAvailability = function(username) { + return RocketChat.settings.get('Accounts_BlockedUsernameList', function(key, value) { + const usernameBlackList = _.map(value.split(','), function(username) { + return username.trim(); + }); + if (usernameBlackList.length !== 0) { + if (usernameBlackList.every(restrictedUsername => { + const regex = new RegExp(`^${ s.escapeRegExp(restrictedUsername) }$`, 'i'); + return !regex.test(s.trim(s.escapeRegExp(username))); + })) { + return !Meteor.users.findOne({ + username: { + $regex: new RegExp(`^${ s.trim(s.escapeRegExp(username)) }$`, 'i') + } + }); + } + return false; + } + }); +}; diff --git a/packages/rocketchat-lib/server/functions/sendMessage.coffee b/packages/rocketchat-lib/server/functions/sendMessage.coffee deleted file mode 100644 index bdce87faba..0000000000 --- a/packages/rocketchat-lib/server/functions/sendMessage.coffee +++ /dev/null @@ -1,50 +0,0 @@ -RocketChat.sendMessage = (user, message, room, upsert = false) -> - if not user or not message or not room._id - return false - - unless message.ts? - message.ts = new Date() - - message.u = _.pick user, ['_id','username'] - - if not Match.test(message.msg, String) - message.msg = '' - - message.rid = room._id - - if not room.usernames? || room.usernames.length is 0 - updated_room = RocketChat.models.Rooms.findOneById(room._id) - if updated_room? - room = updated_room - else - room.usernames = [] - - if message.parseUrls isnt false - if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g - message.urls = urls.map (url) -> url: url - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - # Avoid saving sandstormSessionId to the database - sandstormSessionId = null - if message.sandstormSessionId - sandstormSessionId = message.sandstormSessionId - delete message.sandstormSessionId - - if message._id? and upsert - _id = message._id - delete message._id - RocketChat.models.Messages.upsert {_id: _id, 'u._id': message.u._id}, message - message._id = _id - else - message._id = RocketChat.models.Messages.insert message - - ### - Defer other updates as their return is not interesting to the user - ### - Meteor.defer -> - # Execute all callbacks - message.sandstormSessionId = sandstormSessionId - RocketChat.callbacks.run 'afterSaveMessage', message, room - - return message diff --git a/packages/rocketchat-lib/server/functions/sendMessage.js b/packages/rocketchat-lib/server/functions/sendMessage.js new file mode 100644 index 0000000000..d0e4181e73 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/sendMessage.js @@ -0,0 +1,59 @@ +RocketChat.sendMessage = function(user, message, room, upsert = false) { + if (!user || !message || !room._id) { + return false; + } + if (message.ts == null) { + message.ts = new Date(); + } + message.u = _.pick(user, ['_id', 'username', 'name']); + if (!Match.test(message.msg, String)) { + message.msg = ''; + } + message.rid = room._id; + if (room.usernames || room.usernames.length === 0) { + const updated_room = RocketChat.models.Rooms.findOneById(room._id); + if (updated_room != null) { + room = updated_room; + } else { + room.usernames = []; + } + } + if (message.parseUrls !== false) { + const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g); + if (urls) { + message.urls = urls.map(function(url) { + return { + url + }; + }); + } + } + message = RocketChat.callbacks.run('beforeSaveMessage', message); + // Avoid saving sandstormSessionId to the database + let sandstormSessionId = null; + if (message.sandstormSessionId) { + sandstormSessionId = message.sandstormSessionId; + delete message.sandstormSessionId; + } + if (message._id && upsert) { + const _id = message._id; + delete message._id; + RocketChat.models.Messages.upsert({ + _id, + 'u._id': message.u._id + }, message); + message._id = _id; + } else { + message._id = RocketChat.models.Messages.insert(message); + } + + /* + Defer other updates as their return is not interesting to the user + */ + Meteor.defer(() => { + // Execute all callbacks + message.sandstormSessionId = sandstormSessionId; + return RocketChat.callbacks.run('afterSaveMessage', message, room); + }); + return message; +}; diff --git a/packages/rocketchat-lib/server/functions/setUsername.coffee b/packages/rocketchat-lib/server/functions/setUsername.coffee deleted file mode 100644 index a3afa4a49e..0000000000 --- a/packages/rocketchat-lib/server/functions/setUsername.coffee +++ /dev/null @@ -1,77 +0,0 @@ -RocketChat._setUsername = (userId, username) -> - username = s.trim username - if not userId or not username - return false - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test username - return false - - user = RocketChat.models.Users.findOneById userId - - # User already has desired username, return - if user.username is username - return user - - previousUsername = user.username - - # Check username availability or if the user already owns a different casing of the name - if ( !previousUsername or !(username.toLowerCase() == previousUsername.toLowerCase())) - unless RocketChat.checkUsernameAvailability username - return false - - # If first time setting username, send Enrollment Email - try - if not previousUsername and user.emails?.length > 0 and RocketChat.settings.get 'Accounts_Enrollment_Email' - Accounts.sendEnrollmentEmail(user._id) - catch error - - user.username = username - - # If first time setting username, check if should set default avatar - if not previousUsername and RocketChat.settings.get('Accounts_SetDefaultAvatar') is true - avatarSuggestions = getAvatarSuggestionForUser user - for service, avatarData of avatarSuggestions - if service isnt 'gravatar' - RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service) - gravatar = null - break - else - gravatar = avatarData - if gravatar? - RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar') - - # Username is available; if coming from old username, update all references - if previousUsername - RocketChat.models.Messages.updateAllUsernamesByUserId user._id, username - RocketChat.models.Messages.updateUsernameOfEditByUserId user._id, username - - RocketChat.models.Messages.findByMention(previousUsername).forEach (msg) -> - updatedMsg = msg.msg.replace(new RegExp("@#{previousUsername}", "ig"), "@#{username}") - RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername msg._id, previousUsername, username, updatedMsg - - RocketChat.models.Rooms.replaceUsername previousUsername, username - RocketChat.models.Rooms.replaceMutedUsername previousUsername, username - RocketChat.models.Rooms.replaceUsernameOfUserByUserId user._id, username - - RocketChat.models.Subscriptions.setUserUsernameByUserId user._id, username - RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName previousUsername, username - - rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent("#{previousUsername}.jpg")) - if rs? - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{username}.jpg") - ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{username}.jpg"), rs.contentType - ws.on 'end', Meteor.bindEnvironment -> - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{previousUsername}.jpg") - rs.readStream.pipe(ws) - - # Set new username - RocketChat.models.Users.setUsername user._id, username - return user - -RocketChat.setUsername = RocketChat.RateLimiter.limitFunction RocketChat._setUsername, 1, 60000, - 0: () -> return not Meteor.userId() or not RocketChat.authz.hasPermission(Meteor.userId(), 'edit-other-user-info') # Administrators have permission to change others usernames, so don't limit those diff --git a/packages/rocketchat-lib/server/functions/setUsername.js b/packages/rocketchat-lib/server/functions/setUsername.js new file mode 100644 index 0000000000..455e6cfbc6 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/setUsername.js @@ -0,0 +1,85 @@ + +RocketChat._setUsername = function(userId, u) { + const username = s.trim(u); + if (!userId || !username) { + return false; + } + let nameValidation; + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(username)) { + return false; + } + const user = RocketChat.models.Users.findOneById(userId); + // User already has desired username, return + if (user.username === username) { + return user; + } + const previousUsername = user.username; + // Check username availability or if the user already owns a different casing of the name + if (!previousUsername || !(username.toLowerCase() === previousUsername.toLowerCase())) { + if (!RocketChat.checkUsernameAvailability(username)) { + return false; + } + } + //If first time setting username, send Enrollment Email + try { + if (!previousUsername && user.emails && user.emails.length > 0 && RocketChat.settings.get('Accounts_Enrollment_Email')) { + Accounts.sendEnrollmentEmail(user._id); + } + } catch (e) { + console.error(e); + } + /* globals getAvatarSuggestionForUser */ + user.username = username; + if (!previousUsername && RocketChat.settings.get('Accounts_SetDefaultAvatar') === true) { + const avatarSuggestions = getAvatarSuggestionForUser(user); + let gravatar; + Object.keys(avatarSuggestions).some(service => { + const avatarData = avatarSuggestions[service]; + if (service !== 'gravatar') { + RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service); + gravatar = null; + return true; + } else { + gravatar = avatarData; + } + }); + if (gravatar != null) { + RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar'); + } + } + // Username is available; if coming from old username, update all references + if (previousUsername) { + RocketChat.models.Messages.updateAllUsernamesByUserId(user._id, username); + RocketChat.models.Messages.updateUsernameOfEditByUserId(user._id, username); + RocketChat.models.Messages.findByMention(previousUsername).forEach(function(msg) { + const updatedMsg = msg.msg.replace(new RegExp(`@${ previousUsername }`, 'ig'), `@${ username }`); + return RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); + }); + RocketChat.models.Rooms.replaceUsername(previousUsername, username); + RocketChat.models.Rooms.replaceMutedUsername(previousUsername, username); + RocketChat.models.Rooms.replaceUsernameOfUserByUserId(user._id, username); + RocketChat.models.Subscriptions.setUserUsernameByUserId(user._id, username); + RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName(previousUsername, username); + const rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ previousUsername }.jpg`)); + if (rs != null) { + RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ username }.jpg`)); + const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ username }.jpg`), rs.contentType); + ws.on('end', Meteor.bindEnvironment(() => RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ previousUsername }.jpg`)))); + rs.readStream.pipe(ws); + } + } + // Set new username* + RocketChat.models.Users.setUsername(user._id, username); + return user; +}; + +RocketChat.setUsername = RocketChat.RateLimiter.limitFunction(RocketChat._setUsername, 1, 60000, { + [0](userId) { + return !userId || !RocketChat.authz.hasPermission(userId, 'edit-other-user-info'); + } +}); diff --git a/packages/rocketchat-lib/server/functions/settings.coffee b/packages/rocketchat-lib/server/functions/settings.coffee deleted file mode 100644 index 1098cd7941..0000000000 --- a/packages/rocketchat-lib/server/functions/settings.coffee +++ /dev/null @@ -1,248 +0,0 @@ -blockedSettings = {} -process.env.SETTINGS_BLOCKED?.split(',').forEach (settingId) -> - blockedSettings[settingId] = 1 - -hiddenSettings = {} -process.env.SETTINGS_HIDDEN?.split(',').forEach (settingId) -> - hiddenSettings[settingId] = 1 - -RocketChat.settings._sorter = {} - -### -# Add a setting -# @param {String} _id -# @param {Mixed} value -# @param {Object} setting -### -RocketChat.settings.add = (_id, value, options = {}) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id or - not value? and not process?.env?['OVERWRITE_SETTING_' + _id]? - return false - - RocketChat.settings._sorter[options.group] ?= 0 - - options.packageValue = value - options.valueSource = 'packageValue' - options.hidden = false - options.blocked = options.blocked || false - options.sorter ?= RocketChat.settings._sorter[options.group]++ - - if options.enableQuery? - options.enableQuery = JSON.stringify options.enableQuery - - if options.i18nDefaultQuery? - options.i18nDefaultQuery = JSON.stringify options.i18nDefaultQuery - - if process?.env?[_id]? - value = process.env[_id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - else if Meteor.settings?[_id]? - value = Meteor.settings[_id] - options.meteorSettingsValue = value - options.valueSource = 'meteorSettingsValue' - - if not options.i18nLabel? - options.i18nLabel = _id - - # Default description i18n key will be the setting name + "_Description" (eg: LDAP_Enable -> LDAP_Enable_Description) - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - if process?.env?['OVERWRITE_SETTING_' + _id]? - value = process.env['OVERWRITE_SETTING_' + _id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.value = value - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - updateOperations = - $set: options - $setOnInsert: - createdAt: new Date - - if options.editor? - updateOperations.$setOnInsert.editor = options.editor - delete options.editor - - if not options.value? - if options.force is true - updateOperations.$set.value = options.packageValue - else - updateOperations.$setOnInsert.value = value - - query = _.extend { _id: _id }, updateOperations.$set - - if not options.section? - updateOperations.$unset = { section: 1 } - query.section = { $exists: false } - - existantSetting = RocketChat.models.Settings.db.findOne(query) - - if existantSetting? - if not existantSetting.editor? and updateOperations.$setOnInsert.editor? - updateOperations.$set.editor = updateOperations.$setOnInsert.editor - delete updateOperations.$setOnInsert.editor - else - updateOperations.$set.ts = new Date - - return RocketChat.models.Settings.upsert { _id: _id }, updateOperations - - - -### -# Add a setting group -# @param {String} _id -### -RocketChat.settings.addGroup = (_id, options = {}, cb) -> - # console.log '[functions] RocketChat.settings.addGroup -> '.green, 'arguments:', arguments - - if not _id - return false - - if _.isFunction(options) - cb = options - options = {} - - if not options.i18nLabel? - options.i18nLabel = _id - - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - options.ts = new Date - options.blocked = false - options.hidden = false - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - RocketChat.models.Settings.upsert { _id: _id }, - $set: options - $setOnInsert: - type: 'group' - createdAt: new Date - - if cb? - cb.call - add: (id, value, options = {}) -> - options.group = _id - RocketChat.settings.add id, value, options - - section: (section, cb) -> - cb.call - add: (id, value, options = {}) -> - options.group = _id - options.section = section - RocketChat.settings.add id, value, options - - return - - -### -# Remove a setting by id -# @param {String} _id -### -RocketChat.settings.removeById = (_id) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id - return false - - return RocketChat.models.Settings.removeById _id - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.updateById = (_id, value, editor) -> - # console.log '[functions] RocketChat.settings.updateById -> '.green, 'arguments:', arguments - - if not _id or not value? - return false - - if editor? - return RocketChat.models.Settings.updateValueAndEditorById _id, value, editor - - return RocketChat.models.Settings.updateValueById _id, value - - -### -# Update options of a setting by id -# @param {String} _id -### -RocketChat.settings.updateOptionsById = (_id, options) -> - # console.log '[functions] RocketChat.settings.updateOptionsById -> '.green, 'arguments:', arguments - - if not _id or not options? - return false - - return RocketChat.models.Settings.updateOptionsById _id, options - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.clearById = (_id) -> - # console.log '[functions] RocketChat.settings.clearById -> '.green, 'arguments:', arguments - - if not _id? - return false - - return RocketChat.models.Settings.updateValueById _id, undefined - - -### -# Update a setting by id -### -RocketChat.settings.init = -> - RocketChat.settings.initialLoad = true - RocketChat.models.Settings.find().observe - added: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - changed: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - removed: (record) -> - delete Meteor.settings[record._id] - if record.env is true - delete process.env[record._id] - RocketChat.settings.load record._id, undefined, RocketChat.settings.initialLoad - RocketChat.settings.initialLoad = false - - for fn in RocketChat.settings.afterInitialLoad - fn(Meteor.settings) - - -RocketChat.settings.afterInitialLoad = [] - -RocketChat.settings.onAfterInitialLoad = (fn) -> - RocketChat.settings.afterInitialLoad.push(fn) - if RocketChat.settings.initialLoad is false - fn(Meteor.settings) diff --git a/packages/rocketchat-lib/server/functions/settings.js b/packages/rocketchat-lib/server/functions/settings.js new file mode 100644 index 0000000000..4fa9a3f2b4 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/settings.js @@ -0,0 +1,283 @@ +const blockedSettings = {}; + +if (process.env.SETTINGS_BLOCKED) { + process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings[settingId] = 1); +} + +const hiddenSettings = {}; +if (process.env.SETTINGS_HIDDEN) { + process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings[settingId] = 1); +} + +RocketChat.settings._sorter = {}; + + +/* +* Add a setting +* @param {String} _id +* @param {Mixed} value +* @param {Object} setting +*/ + +RocketChat.settings.add = function(_id, value, options = {}) { + if (options == null) { + options = {}; + } + if (!_id || value == null) { + return false; + } + if (RocketChat.settings._sorter[options.group] == null) { + RocketChat.settings._sorter[options.group] = 0; + } + options.packageValue = value; + options.valueSource = 'packageValue'; + options.hidden = false; + options.blocked = options.blocked || false; + if (options.sorter == null) { + options.sorter = RocketChat.settings._sorter[options.group]++; + } + if (options.enableQuery != null) { + options.enableQuery = JSON.stringify(options.enableQuery); + } + if (options.i18nDefaultQuery != null) { + options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery); + } + if (typeof process !== 'undefined' && process.env && process.env._id) { + let value = process.env[_id]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } else if (Meteor.settings && Meteor.settings) { + const value = Meteor.settings[_id]; + options.meteorSettingsValue = value; + options.valueSource = 'meteorSettingsValue'; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { + let value = process.env[`OVERWRITE_SETTING_${ _id }`]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.value = value; + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } + const updateOperations = { + $set: options, + $setOnInsert: { + createdAt: new Date + } + }; + if (options.editor != null) { + updateOperations.$setOnInsert.editor = options.editor; + delete options.editor; + } + if (options.value == null) { + if (options.force === true) { + updateOperations.$set.value = options.packageValue; + } else { + updateOperations.$setOnInsert.value = value; + } + } + const query = _.extend({ + _id + }, updateOperations.$set); + if (options.section == null) { + updateOperations.$unset = { + section: 1 + }; + query.section = { + $exists: false + }; + } + const existantSetting = RocketChat.models.Settings.db.findOne(query); + if (existantSetting != null) { + if (existantSetting.editor == null && updateOperations.$setOnInsert.editor != null) { + updateOperations.$set.editor = updateOperations.$setOnInsert.editor; + delete updateOperations.$setOnInsert.editor; + } + } else { + updateOperations.$set.ts = new Date; + } + return RocketChat.models.Settings.upsert({ + _id + }, updateOperations); +}; + + +/* +* Add a setting group +* @param {String} _id +*/ + +RocketChat.settings.addGroup = function(_id, options = {}, cb) { + if (!_id) { + return false; + } + if (_.isFunction(options)) { + cb = options; + options = {}; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + options.ts = new Date; + options.blocked = false; + options.hidden = false; + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + RocketChat.models.Settings.upsert({ + _id + }, { + $set: options, + $setOnInsert: { + type: 'group', + createdAt: new Date + } + }); + if (cb != null) { + cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + return RocketChat.settings.add(id, value, options); + }, + section(section, cb) { + return cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + options.section = section; + return RocketChat.settings.add(id, value, options); + } + }); + } + }); + } +}; + + +/* +* Remove a setting by id +* @param {String} _id +*/ + +RocketChat.settings.removeById = function(_id) { + if (!_id) { + return false; + } + return RocketChat.models.Settings.removeById(_id); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateById = function(_id, value, editor) { + if (!_id || value == null) { + return false; + } + if (editor != null) { + return RocketChat.models.Settings.updateValueAndEditorById(_id, value, editor); + } + return RocketChat.models.Settings.updateValueById(_id, value); +}; + + +/* +* Update options of a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateOptionsById = function(_id, options) { + if (!_id || options == null) { + return false; + } + return RocketChat.models.Settings.updateOptionsById(_id, options); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.clearById = function(_id) { + if (_id == null) { + return false; + } + return RocketChat.models.Settings.updateValueById(_id, undefined); +}; + + +/* +* Update a setting by id +*/ + +RocketChat.settings.init = function() { + RocketChat.settings.initialLoad = true; + RocketChat.models.Settings.find().observe({ + added(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + changed(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + removed(record) { + delete Meteor.settings[record._id]; + if (record.env === true) { + delete process.env[record._id]; + } + return RocketChat.settings.load(record._id, undefined, RocketChat.settings.initialLoad); + } + }); + RocketChat.settings.initialLoad = false; + RocketChat.settings.afterInitialLoad.forEach(fn => fn(Meteor.settings)); +}; + +RocketChat.settings.afterInitialLoad = []; + +RocketChat.settings.onAfterInitialLoad = function(fn) { + RocketChat.settings.afterInitialLoad.push(fn); + if (RocketChat.settings.initialLoad === false) { + return fn(Meteor.settings); + } +}; diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee deleted file mode 100644 index 95e8f71a66..0000000000 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment' - -Meteor.methods - sendMessage: (message) -> - - check message, Object - - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'sendMessage' }) - - if message.ts - tsDiff = Math.abs(moment(message.ts).diff()) - if tsDiff > 60000 - throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { method: 'sendMessage', message_ts: message.ts, server_ts: new Date().getTime() }) - else if tsDiff > 10000 - message.ts = new Date() - else - message.ts = new Date() - - if message.msg?.length > RocketChat.settings.get('Message_MaxAllowedSize') - throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { method: 'sendMessage' }) - - user = RocketChat.models.Users.findOneById Meteor.userId(), fields: username: 1, name: 1 - - room = Meteor.call 'canAccessRoom', message.rid, user._id - - if not room - return false - - subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); - if subscription and (subscription.blocked or subscription.blocker) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('room_is_blocked', {}, user.language) - } - return false - - if user.username in (room.muted or []) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('You_have_been_muted', {}, user.language) - } - return false - - message.alias = user.name if not message.alias? and RocketChat.settings.get 'Message_SetNameToAliasEnabled' - if Meteor.settings.public.sandstorm - message.sandstormSessionId = this.connection.sandstormSessionId() - - RocketChat.metrics.messagesSent.inc() # This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - RocketChat.sendMessage user, message, room - -# Limit a user, who does not have the "bot" role, to sending 5 msgs/second -DDPRateLimiter.addRule - type: 'method' - name: 'sendMessage' - userId: (userId) -> - user = RocketChat.models.Users.findOneById(userId) - return true if not user?.roles - return 'bot' not in user.roles -, 5, 1000 diff --git a/packages/rocketchat-lib/server/methods/sendMessage.js b/packages/rocketchat-lib/server/methods/sendMessage.js new file mode 100644 index 0000000000..2f6e41502f --- /dev/null +++ b/packages/rocketchat-lib/server/methods/sendMessage.js @@ -0,0 +1,81 @@ +import moment from 'moment'; + +Meteor.methods({ + sendMessage(message) { + check(message, Object); + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'sendMessage' + }); + } + if (message.ts) { + const tsDiff = Math.abs(moment(message.ts).diff()); + if (tsDiff > 60000) { + throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { + method: 'sendMessage', + message_ts: message.ts, + server_ts: new Date().getTime() + }); + } else if (tsDiff > 10000) { + message.ts = new Date(); + } + } else { + message.ts = new Date(); + } + if (message.msg && message.msg.length > RocketChat.settings.get('Message_MaxAllowedSize')) { + throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { + method: 'sendMessage' + }); + } + const user = RocketChat.models.Users.findOneById(Meteor.userId(), { + fields: { + username: 1, + name: 1 + } + }); + const room = Meteor.call('canAccessRoom', message.rid, user._id); + if (!room) { + return false; + } + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); + if (subscription && subscription.blocked || subscription.blocker) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('room_is_blocked', {}, user.language) + }); + return false; + } + + if ((room.muted||[]).includes(user.username)) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('You_have_been_muted', {}, user.language) + }); + return false; + } + if (message.alias == null && RocketChat.settings.get('Message_SetNameToAliasEnabled')) { + message.alias = user.name; + } + if (Meteor.settings['public'].sandstorm) { + message.sandstormSessionId = this.connection.sandstormSessionId(); + } + RocketChat.metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 + return RocketChat.sendMessage(user, message, room); + } +}); +// Limit a user, who does not have the "bot" role, to sending 5 msgs/second +DDPRateLimiter.addRule({ + type: 'method', + name: 'sendMessage', + userId(userId) { + const user = RocketChat.models.Users.findOneById(userId); + if (user == null || !user.roles) { + return true; + } + return user.roles.includes('bot'); + } +}, 5, 1000); diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js new file mode 100644 index 0000000000..dabf189366 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Messages.js @@ -0,0 +1,580 @@ +RocketChat.models.Messages = new class extends RocketChat.models._Base { + constructor() { + super('message'); + + this.tryEnsureIndex({ 'rid': 1, 'ts': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + this.tryEnsureIndex({ 'editedAt': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'editedBy._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'rid': 1, 't': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); + this.tryEnsureIndex({ 'msg': 'text' }); + this.tryEnsureIndex({ 'file._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mentions.username': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'pinned': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'snippeted': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'location': '2dsphere' }); + this.tryEnsureIndex({ 'slackBotId': 1, 'slackTs': 1 }, { sparse: 1 }); + } + + // FIND + findByMention(username, options) { + const query = {'mentions.username': username}; + + return this.find(query, options); + } + + findVisibleByMentionAndRoomId(username, rid, options) { + const query = { + _hidden: { $ne: true }, + 'mentions.username': username, + rid + }; + + return this.find(query, options); + } + + findVisibleByRoomId(roomId, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdNotContainingTypes(roomId, types, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findInvisibleByRoomId(roomId, options) { + const query = { + _hidden: true, + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdAfterTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampInclusive(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lte: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestamps(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gte: afterTimestamp, + $lte: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampNotContainingTypes(roomId, timestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, afterTimestamp, beforeTimestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleCreatedOrEditedAfterTimestamp(timestamp, options) { + const query = { + _hidden: { $ne: true }, + $or: [{ + ts: { + $gt: timestamp + } + }, + { + 'editedAt': { + $gt: timestamp + } + } + ] + }; + + return this.find(query, options); + } + + findStarredByUserAtRoom(userId, roomId, options) { + const query = { + _hidden: { $ne: true }, + 'starred._id': userId, + rid: roomId + }; + + return this.find(query, options); + } + + findPinnedByRoom(roomId, options) { + const query = { + t: { $ne: 'rm' }, + _hidden: { $ne: true }, + pinned: true, + rid: roomId + }; + + return this.find(query, options); + } + + findSnippetedByRoom(roomId, options) { + const query = { + _hidden: { $ne: true }, + snippeted: true, + rid: roomId + }; + + return this.find(query, options); + } + + getLastTimestamp(options) { + if (options == null) { options = {}; } + const query = { ts: { $exists: 1 } }; + options.sort = { ts: -1 }; + options.limit = 1; + const [message] = this.find(query, options).fetch(); + return message && message.ts; + } + + findByRoomIdAndMessageIds(rid, messageIds, options) { + const query = { + rid, + _id: { + $in: messageIds + } + }; + + return this.find(query, options); + } + + findOneBySlackBotIdAndSlackTs(slackBotId, slackTs) { + const query = { + slackBotId, + slackTs + }; + + return this.findOne(query); + } + + findOneBySlackTs(slackTs) { + const query = {slackTs}; + + return this.findOne(query); + } + + cloneAndSaveAsHistoryById(_id) { + const me = RocketChat.models.Users.findOneById(Meteor.userId()); + const record = this.findOneById(_id); + record._hidden = true; + record.parent = record._id; + record.editedAt = new Date; + record.editedBy = { + _id: Meteor.userId(), + username: me.username + }; + delete record._id; + return this.insert(record); + } + + // UPDATE + setHiddenById(_id, hidden) { + if (hidden == null) { hidden = true; } + const query = {_id}; + + const update = { + $set: { + _hidden: hidden + } + }; + + return this.update(query, update); + } + + setAsDeletedByIdAndUser(_id, user) { + const query = {_id}; + + const update = { + $set: { + msg: '', + t: 'rm', + urls: [], + mentions: [], + attachments: [], + reactions: [], + editedAt: new Date(), + editedBy: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setPinnedByIdAndUserId(_id, pinnedBy, pinned, pinnedAt) { + if (pinned == null) { pinned = true; } + if (pinnedAt == null) { pinnedAt = 0; } + const query = {_id}; + + const update = { + $set: { + pinned, + pinnedAt: pinnedAt || new Date, + pinnedBy + } + }; + + return this.update(query, update); + } + + setSnippetedByIdAndUserId(message, snippetName, snippetedBy, snippeted, snippetedAt) { + if (snippeted == null) { snippeted = true; } + if (snippetedAt == null) { snippetedAt = 0; } + const query = {_id: message._id}; + + const msg = `\`\`\`${ message.msg }\`\`\``; + + const update = { + $set: { + msg, + snippeted, + snippetedAt: snippetedAt || new Date, + snippetedBy, + snippetName + } + }; + + return this.update(query, update); + } + + setUrlsById(_id, urls) { + const query = {_id}; + + const update = { + $set: { + urls + } + }; + + return this.update(query, update); + } + + updateAllUsernamesByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameOfEditByUserId(userId, username) { + const query = {'editedBy._id': userId}; + + const update = { + $set: { + 'editedBy.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameAndMessageOfMentionByIdAndOldUsername(_id, oldUsername, newUsername, newMessage) { + const query = { + _id, + 'mentions.username': oldUsername + }; + + const update = { + $set: { + 'mentions.$.username': newUsername, + 'msg': newMessage + } + }; + + return this.update(query, update); + } + + updateUserStarById(_id, userId, starred) { + let update; + const query = {_id}; + + if (starred) { + update = { + $addToSet: { + starred: { _id: userId } + } + }; + } else { + update = { + $pull: { + starred: { _id: Meteor.userId() } + } + }; + } + + return this.update(query, update); + } + + upgradeEtsToEditAt() { + const query = {ets: { $exists: 1 }}; + + const update = { + $rename: { + 'ets': 'editedAt' + } + }; + + return this.update(query, update, { multi: true }); + } + + setMessageAttachments(_id, attachments) { + const query = {_id}; + + const update = { + $set: { + attachments + } + }; + + return this.update(query, update); + } + + setSlackBotIdAndSlackTs(_id, slackBotId, slackTs) { + const query = {_id}; + + const update = { + $set: { + slackBotId, + slackTs + } + }; + + return this.update(query, update); + } + + + // INSERT + createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) { + const room = RocketChat.models.Rooms.findOneById(roomId, { fields: { sysMes: 1 }}); + if ((room != null ? room.sysMes : undefined) === false) { + return; + } + const record = { + t: type, + rid: roomId, + ts: new Date, + msg: message, + u: { + _id: user._id, + username: user.username + }, + groupable: false + }; + + _.extend(record, extraData); + + record._id = this.insertOrUpsert(record); + RocketChat.models.Rooms.incMsgCountById(room._id, 1); + return record; + } + + createUserJoinWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData); + } + + createUserLeaveWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ul', roomId, message, user, extraData); + } + + createUserRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ru', roomId, message, user, extraData); + } + + createUserAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('au', roomId, message, user, extraData); + } + + createCommandWithRoomIdAndUser(command, roomId, user, extraData) { + return this.createWithTypeRoomIdMessageAndUser('command', roomId, command, user, extraData); + } + + createUserMutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-muted', roomId, message, user, extraData); + } + + createUserUnmutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-unmuted', roomId, message, user, extraData); + } + + createNewModeratorWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-moderator', roomId, message, user, extraData); + } + + createModeratorRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('moderator-removed', roomId, message, user, extraData); + } + + createNewOwnerWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-owner', roomId, message, user, extraData); + } + + createOwnerRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('owner-removed', roomId, message, user, extraData); + } + + createSubscriptionRoleAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-added', roomId, message, user, extraData); + } + + createSubscriptionRoleRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData); + } + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = {rid: roomId}; + + return this.remove(query); + } + + removeByUserId(userId) { + const query = {'u._id': userId}; + + return this.remove(query); + } + + getMessageByFileId(fileID) { + return this.findOne({ 'file._id': fileID }); + } +}; diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js new file mode 100644 index 0000000000..763155e1c8 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -0,0 +1,760 @@ +class ModelRooms extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'name': 1 }, { unique: 1, sparse: 1 }); + this.tryEnsureIndex({ 'default': 1 }); + this.tryEnsureIndex({ 'usernames': 1 }); + this.tryEnsureIndex({ 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + + this.cache.ignoreUpdatedFields.push('msgs', 'lm'); + this.cache.ensureIndex(['t', 'name'], 'unique'); + this.cache.options = {fields: {usernames: 0}}; + } + + findOneByIdOrName(_idOrName, options) { + const query = { + $or: [{ + _id: _idOrName + }, { + name: _idOrName + }] + }; + + return this.findOne(query, options); + } + + findOneByImportId(_id, options) { + const query = {importIds: _id}; + + return this.findOne(query, options); + } + + findOneByName(name, options) { + const query = {name}; + + return this.findOne(query, options); + } + + findOneByNameAndType(name, type, options) { + const query = { + name, + t: type + }; + + return this.findOne(query, options); + } + + findOneByIdContainingUsername(_id, username, options) { + const query = { + _id, + usernames: username + }; + + return this.findOne(query, options); + } + + findOneByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + name, + t: type, + usernames: { + $ne: username + } + }; + + return this.findOne(query, options); + } + + + // FIND + + findById(roomId, options) { + return this.find({ _id: roomId }, options); + } + + findByIds(roomIds, options) { + return this.find({ _id: {$in: [].concat(roomIds)} }, options); + } + + findByType(type, options) { + const query = {t: type}; + + return this.find(query, options); + } + + findByTypes(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByUserId(userId, options) { + const query = {'u._id': userId}; + + return this.find(query, options); + } + + findBySubscriptionUserId(userId, options) { + let data; + if (this.useCache) { + data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + data = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + data = data.map(item => item.rid); + + const query = { + _id: { + $in: data + } + }; + + return this.find(query, options); + } + + findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) { + if (this.useCache) { + let data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + data = data.filter(item => item._updatedAt > _updatedAt); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + let ids = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + ids = ids.map(item => item.rid); + + const query = { + _id: { + $in: ids + }, + _updatedAt: { + $gt: _updatedAt + } + }; + + return this.find(query, options); + } + + findByNameContaining(name, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameContainingTypesWithUsername(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const $or = []; + for (const type of Array.from(types)) { + const obj = {name: nameRegex, t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findContainingTypesWithUsername(types, options) { + + const $or = []; + for (const type of Array.from(types)) { + const obj = {t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findByNameContainingAndTypes(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + t: type, + name, + usernames: { + $ne: username + } + }; + + return this.find(query, options); + } + + findByNameStartingAndTypes(name, types, options) { + const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByDefaultAndTypes(defaultValue, types, options) { + const query = { + default: defaultValue, + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeContainingUsername(type, username, options) { + const query = { + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeContainingUsernames(type, username, options) { + const query = { + t: type, + usernames: { $all: [].concat(username) } + }; + + return this.find(query, options); + } + + findByTypesAndNotUserIdContainingUsername(types, userId, username, options) { + const query = { + t: { + $in: types + }, + uid: { + $ne: userId + }, + usernames: username + }; + + return this.find(query, options); + } + + findByContainingUsername(username, options) { + const query = {usernames: username}; + + return this.find(query, options); + } + + findByTypeAndName(type, name, options) { + if (this.useCache) { + return this.cache.findByIndex('t,name', [type, name], options); + } + + const query = { + name, + t: type + }; + + return this.find(query, options); + } + + findByTypeAndNameContainingUsername(type, name, username, options) { + const query = { + name, + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeAndArchivationState(type, archivationstate, options) { + const query = {t: type}; + + if (archivationstate) { + query.archived = true; + } else { + query.archived = { $ne: true }; + } + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + archiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: true + } + }; + + return this.update(query, update); + } + + unarchiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: false + } + }; + + return this.update(query, update); + } + + addUsernameById(_id, username, muted) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: username + } + }; + + if (muted) { + update.$addToSet.muted = username; + } + + return this.update(query, update); + } + + addUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: { + $each: usernames + } + } + }; + + return this.update(query, update); + } + + addUsernameByName(name, username) { + const query = {name}; + + const update = { + $addToSet: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernameById(_id, username) { + const query = {_id}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $pull: { + usernames: { + $in: usernames + } + } + }; + + return this.update(query, update); + } + + removeUsernameFromAll(username) { + const query = {usernames: username}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update, { multi: true }); + } + + removeUsernameByName(name, username) { + const query = {name}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + setNameById(_id, name) { + const query = {_id}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update); + } + + incMsgCountById(_id, inc) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + incMsgCountAndSetLastMessageTimestampById(_id, inc, lastMessageTimestamp) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $set: { + lm: lastMessageTimestamp + }, + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + replaceUsername(previousUsername, username) { + const query = {usernames: previousUsername}; + + const update = { + $set: { + 'usernames.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceMutedUsername(previousUsername, username) { + const query = {muted: previousUsername}; + + const update = { + $set: { + 'muted.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceUsernameOfUserByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setJoinCodeById(_id, joinCode) { + let update; + const query = {_id}; + + if ((joinCode != null ? joinCode.trim() : undefined) !== '') { + update = { + $set: { + joinCodeRequired: true, + joinCode + } + }; + } else { + update = { + $set: { + joinCodeRequired: false + }, + $unset: { + joinCode: 1 + } + }; + } + + return this.update(query, update); + } + + setUserById(_id, user) { + const query = {_id}; + + const update = { + $set: { + u: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setTypeById(_id, type) { + const query = {_id}; + const update = { + $set: { + t: type + } + }; + if (type === 'p') { + update.$unset = {default: ''}; + } + + return this.update(query, update); + } + + setTopicById(_id, topic) { + const query = {_id}; + + const update = { + $set: { + topic + } + }; + + return this.update(query, update); + } + + setAnnouncementById(_id, announcement) { + const query = {_id}; + + const update = { + $set: { + announcement + } + }; + + return this.update(query, update); + } + + muteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $addToSet: { + muted: username + } + }; + + return this.update(query, update); + } + + unmuteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $pull: { + muted: username + } + }; + + return this.update(query, update); + } + + saveDefaultById(_id, defaultValue) { + const query = {_id}; + + const update = { + $set: { + default: defaultValue === 'true' + } + }; + + return this.update(query, update); + } + + setTopicAndTagsById(_id, topic, tags) { + const setData = {}; + const unsetData = {}; + + if (topic != null) { + if (!_.isEmpty(s.trim(topic))) { + setData.topic = s.trim(topic); + } else { + unsetData.topic = 1; + } + } + + if (tags != null) { + if (!_.isEmpty(s.trim(tags))) { + setData.tags = s.trim(tags).split(',').map(tag => s.trim(tag)); + } else { + unsetData.tags = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return; + } + + return this.update({ _id }, update); + } + + // INSERT + createWithTypeNameUserAndUsernames(type, name, user, usernames, extraData) { + const room = { + name, + t: type, + usernames, + msgs: 0, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(room, extraData); + + room._id = this.insert(room); + return room; + } + + createWithIdTypeAndName(_id, type, name, extraData) { + const room = { + _id, + ts: new Date(), + t: type, + name, + usernames: [], + msgs: 0 + }; + + _.extend(room, extraData); + + this.insert(room); + return room; + } + + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByTypeContainingUsername(type, username) { + const query = { + t: type, + usernames: username + }; + + return this.remove(query); + } +} + +RocketChat.models.Rooms = new ModelRooms('room', true); diff --git a/packages/rocketchat-lib/server/models/Settings.js b/packages/rocketchat-lib/server/models/Settings.js new file mode 100644 index 0000000000..7da29bee37 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Settings.js @@ -0,0 +1,178 @@ +class ModelSettings extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'blocked': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'hidden': 1 }, { sparse: 1 }); + } + + // FIND + findById(_id) { + const query = {_id}; + + return this.find(query); + } + + findOneNotHiddenById(_id) { + const query = { + _id, + hidden: { $ne: true } + }; + + return this.findOne(query); + } + + findByIds(_id = []) { + _id = [].concat(_id); + + const query = { + _id: { + $in: _id + } + }; + + return this.find(query); + } + + findByRole(role, options) { + const query = {role}; + + return this.find(query, options); + } + + findPublic(options) { + const query = {public: true}; + + return this.find(query, options); + } + + findNotHiddenPublic(ids = []) { + const filter = { + hidden: { $ne: true }, + public: true + }; + + if (ids.length > 0) { + filter._id = + {$in: ids}; + } + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPublicUpdatedAfter(updatedAt) { + const filter = { + hidden: { $ne: true }, + public: true, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPrivate() { + return this.find({ + hidden: { $ne: true }, + public: { $ne: true } + }); + } + + findNotHidden(options) { + return this.find({ hidden: { $ne: true } }, options); + } + + findNotHiddenUpdatedAfter(updatedAt) { + return this.find({ + hidden: { $ne: true }, + _updatedAt: { + $gt: updatedAt + } + }); + } + + // UPDATE + updateValueById(_id, value) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateValueAndEditorById(_id, value, editor) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value, + editor + } + }; + + return this.update(query, update); + } + + updateValueNotHiddenById(_id, value) { + const query = { + _id, + hidden: { $ne: true }, + blocked: { $ne: true } + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateOptionsById(_id, options) { + const query = { + blocked: { $ne: true }, + _id + }; + + const update = {$set: options}; + + return this.update(query, update); + } + + // INSERT + createWithIdAndValue(_id, value) { + const record = { + _id, + value, + _createdAt: new Date + }; + + return this.insert(record); + } + + // REMOVE + removeById(_id) { + const query = { + blocked: { $ne: true }, + _id + }; + + return this.remove(query); + } +} + +RocketChat.models.Settings = new ModelSettings('settings', true); diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js new file mode 100644 index 0000000000..67986040e0 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -0,0 +1,570 @@ +class ModelSubscriptions extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'rid': 1, 'u._id': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'rid': 1, 'alert': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'rid': 1, 'roles': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1, 'code': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'open': 1 }); + this.tryEnsureIndex({ 'alert': 1 }); + this.tryEnsureIndex({ 'unread': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'ls': 1 }); + this.tryEnsureIndex({ 'audioNotification': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'desktopNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mobilePushNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'emailNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslate': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslateLanguage': 1 }, { sparse: 1 }); + + this.cache.ensureIndex('rid', 'array'); + this.cache.ensureIndex('u._id', 'array'); + this.cache.ensureIndex('name', 'array'); + this.cache.ensureIndex(['rid', 'u._id'], 'unique'); + this.cache.ensureIndex(['name', 'u._id'], 'unique'); + } + + + // FIND ONE + findOneByRoomIdAndUserId(roomId, userId) { + if (this.useCache) { + return this.cache.findByIndex('rid,u._id', [roomId, userId]).fetch(); + } + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.findOne(query); + } + + findOneByRoomNameAndUserId(roomName, userId) { + if (this.useCache) { + return this.cache.findByIndex('name,u._id', [roomName, userId]).fetch(); + } + const query = { + name: roomName, + 'u._id': userId + }; + + return this.findOne(query); + } + + // FIND + findByUserId(userId, options) { + if (this.useCache) { + return this.cache.findByIndex('u._id', userId, options); + } + + const query = + {'u._id': userId}; + + return this.find(query, options); + } + + findByUserIdUpdatedAfter(userId, updatedAt, options) { + const query = { + 'u._id': userId, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(query, options); + } + + // FIND + findByRoomIdAndRoles(roomId, roles, options) { + roles = [].concat(roles); + const query = { + 'rid': roomId, + 'roles': { $in: roles } + }; + + return this.find(query, options); + } + + findByType(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeAndUserId(type, userId, options) { + const query = { + t: type, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByTypeNameAndUserId(type, name, userId, options) { + const query = { + t: type, + name, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByRoomId(roomId, options) { + if (this.useCache) { + return this.cache.findByIndex('rid', roomId, options); + } + + const query = + {rid: roomId}; + + return this.find(query, options); + } + + findByRoomIdAndNotUserId(roomId, userId, options) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + return this.find(query, options); + } + + getLastSeen(options) { + if (options == null) { options = {}; } + const query = { ls: { $exists: 1 } }; + options.sort = { ls: -1 }; + options.limit = 1; + const [subscription] = this.find(query, options).fetch(); + return subscription && subscription.ls; + } + + findByRoomIdAndUserIds(roomId, userIds) { + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + return this.find(query); + } + + // UPDATE + archiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: false, + archived: true + } + }; + + return this.update(query, update, { multi: true }); + } + + unarchiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: true, + archived: false + } + }; + + return this.update(query, update, { multi: true }); + } + + hideByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + alert: false, + open: false + } + }; + + return this.update(query, update); + } + + openByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true + } + }; + + return this.update(query, update); + } + + setAsReadByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: false, + unread: 0, + ls: new Date + } + }; + + return this.update(query, update); + } + + setAsUnreadByRoomIdAndUserId(roomId, userId, firstMessageUnreadTimestamp) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: true, + ls: firstMessageUnreadTimestamp + } + }; + + return this.update(query, update); + } + + setFavoriteByRoomIdAndUserId(roomId, userId, favorite) { + if (favorite == null) { favorite = true; } + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + f: favorite + } + }; + + return this.update(query, update); + } + + updateNameAndAlertByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name, + alert: true + } + }; + + return this.update(query, update, { multi: true }); + } + + updateNameByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + setUserUsernameByUserId(userId, username) { + const query = + {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setNameForDirectRoomsWithOldName(oldName, name) { + const query = { + name: oldName, + t: 'd' + }; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadOfDirectForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + t: 'd', + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdAndUserIds(roomId, userIds, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + setAlertForRoomIdExcludingUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + }, + $or: [ + { alert: { $ne: true } }, + { open: { $ne: true } } + ] + }; + + const update = { + $set: { + alert: true, + open: true + } + }; + + return this.update(query, update, { multi: true }); + } + + setBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $set: { + blocked: true + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $set: { + blocker: true + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + unsetBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $unset: { + blocked: 1 + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $unset: { + blocker: 1 + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + updateTypeByRoomId(roomId, type) { + const query = + {rid: roomId}; + + const update = { + $set: { + t: type + } + }; + + return this.update(query, update, { multi: true }); + } + + addRoleById(_id, role) { + const query = + {_id}; + + const update = { + $addToSet: { + roles: role + } + }; + + return this.update(query, update); + } + + removeRoleById(_id, role) { + const query = + {_id}; + + const update = { + $pull: { + roles: role + } + }; + + return this.update(query, update); + } + + setArchivedByUsername(username, archived) { + const query = { + t: 'd', + name: username + }; + + const update = { + $set: { + archived + } + }; + + return this.update(query, update, { multi: true }); + } + + // INSERT + createWithRoomAndUser(room, user, extraData) { + const subscription = { + open: false, + alert: false, + unread: 0, + ts: room.ts, + rid: room._id, + name: room.name, + t: room.t, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(subscription, extraData); + + return this.insert(subscription); + } + + + // REMOVE + removeByUserId(userId) { + const query = + {'u._id': userId}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = + {rid: roomId}; + + return this.remove(query); + } + + removeByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.remove(query); + } +} + +RocketChat.models.Subscriptions = new ModelSubscriptions('subscription', true); diff --git a/packages/rocketchat-lib/server/models/Uploads.js b/packages/rocketchat-lib/server/models/Uploads.js new file mode 100644 index 0000000000..252d48fb87 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Uploads.js @@ -0,0 +1,91 @@ +RocketChat.models.Uploads = new class extends RocketChat.models._Base { + constructor() { + super('uploads'); + + this.tryEnsureIndex({ 'rid': 1 }); + this.tryEnsureIndex({ 'uploadedAt': 1 }); + } + + findNotHiddenFilesOfRoom(roomId, limit) { + const fileQuery = { + rid: roomId, + complete: true, + uploading: false, + _hidden: { + $ne: true + } + }; + + const fileOptions = { + limit, + sort: { + uploadedAt: -1 + }, + fields: { + _id: 1, + userId: 1, + rid: 1, + name: 1, + description: 1, + type: 1, + url: 1, + uploadedAt: 1 + } + }; + + return this.find(fileQuery, fileOptions); + } + + insertFileInit(roomId, userId, store, file, extra) { + const fileData = { + rid: roomId, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + file = this.model.direct.insert(fileData); + } else { + file = this.insert(fileData); + } + + return file; + } + + updateFileComplete(fileId, userId, file) { + let result; + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + result = this.model.direct.update(filter, update); + } else { + result = this.update(filter, update); + } + + return result; + } +}; diff --git a/packages/rocketchat-lib/server/models/Users.js b/packages/rocketchat-lib/server/models/Users.js new file mode 100644 index 0000000000..941416475c --- /dev/null +++ b/packages/rocketchat-lib/server/models/Users.js @@ -0,0 +1,538 @@ +class ModelUsers extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'roles': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'name': 1 }); + this.tryEnsureIndex({ 'lastLogin': 1 }); + this.tryEnsureIndex({ 'status': 1 }); + this.tryEnsureIndex({ 'active': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'statusConnection': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'type': 1 }); + + this.cache.ensureIndex('username', 'unique'); + } + + findOneByImportId(_id, options) { + return this.findOne({ importIds: _id }, options); + } + + findOneByUsername(username, options) { + const query = {username}; + + return this.findOne(query, options); + } + + findOneByEmailAddress(emailAddress, options) { + const query = {'emails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i')}; + + return this.findOne(query, options); + } + + findOneAdmin(admin, options) { + const query = {admin}; + + return this.findOne(query, options); + } + + findOneByIdAndLoginToken(_id, token, options) { + const query = { + _id, + 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token) + }; + + return this.findOne(query, options); + } + + + // FIND + findById(userId) { + const query = {_id: userId}; + + return this.find(query); + } + + findUsersNotOffline(options) { + const query = { + username: { + $exists: 1 + }, + status: { + $in: ['online', 'away', 'busy'] + } + }; + + return this.find(query, options); + } + + + findByUsername(username, options) { + const query = {username}; + + return this.find(query, options); + } + + findUsersByUsernamesWithHighlights(usernames, options) { + if (this.useCache) { + const result = { + fetch() { + return RocketChat.models.Users.getDynamicView('highlights').data().filter(record => usernames.indexOf(record.username) > -1); + }, + count() { + return result.fetch().length; + }, + forEach(fn) { + return result.fetch().forEach(fn); + } + }; + return result; + } + + const query = { + username: { $in: usernames }, + 'settings.preferences.highlights.0': { + $exists: true + } + }; + + return this.find(query, options); + } + + findActiveByUsernameOrNameRegexWithExceptions(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $or: [{ + username: termRegex + }, { + name: termRegex + }], + active: true, + type: { + $in: ['user', 'bot'] + }, + $and: [{ + username: { + $exists: true + } + }, { + username: { + $nin: exceptions + } + }] + }; + + return this.find(query, options); + } + + findByActiveUsersExcept(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $and: [ + { + active: true, + $or: [ + { + username: termRegex + }, + { + name: termRegex + } + ] + }, + { + username: { $exists: true, $nin: exceptions } + } + ] + }; + + return this.find(query, options); + } + + findUsersByNameOrUsername(nameOrUsername, options) { + const query = { + username: { + $exists: 1 + }, + + $or: [ + {name: nameOrUsername}, + {username: nameOrUsername} + ], + + type: { + $in: ['user'] + } + }; + + return this.find(query, options); + } + + findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) { + const query = { + $or: [ + {name: usernameNameOrEmailAddress}, + {username: usernameNameOrEmailAddress}, + {'emails.address': usernameNameOrEmailAddress} + ], + type: { + $in: ['user', 'bot'] + } + }; + + return this.find(query, options); + } + + findLDAPUsers(options) { + const query = {ldap: true}; + + return this.find(query, options); + } + + findCrowdUsers(options) { + const query = {crowd: true}; + + return this.find(query, options); + } + + getLastLogin(options) { + if (options == null) { options = {}; } + const query = { lastLogin: { $exists: 1 } }; + options.sort = { lastLogin: -1 }; + options.limit = 1; + const [user] = this.find(query, options).fetch(); + return user && user.lastLogin; + } + + findUsersByUsernames(usernames, options) { + const query = { + username: { + $in: usernames + } + }; + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + updateLastLoginById(_id) { + const update = { + $set: { + lastLogin: new Date + } + }; + + return this.update(_id, update); + } + + setServiceId(_id, serviceName, serviceId) { + const update = + {$set: {}}; + + const serviceIdKey = `services.${ serviceName }.id`; + update.$set[serviceIdKey] = serviceId; + + return this.update(_id, update); + } + + setUsername(_id, username) { + const update = + {$set: {username}}; + + return this.update(_id, update); + } + + setEmail(_id, email) { + const update = { + $set: { + emails: [{ + address: email, + verified: false + } + ] + } + }; + + return this.update(_id, update); + } + + setEmailVerified(_id, email) { + const query = { + _id, + emails: { + $elemMatch: { + address: email, + verified: false + } + } + }; + + const update = { + $set: { + 'emails.$.verified': true + } + }; + + return this.update(query, update); + } + + setName(_id, name) { + const update = { + $set: { + name + } + }; + + return this.update(_id, update); + } + + setCustomFields(_id, fields) { + const values = {}; + Object.keys(fields).reduce(key => { + const value = fields[key]; + values[`customFields.${ key }`] = value; + }); + + const update = {$set: values}; + + return this.update(_id, update); + } + + setAvatarOrigin(_id, origin) { + const update = { + $set: { + avatarOrigin: origin + } + }; + + return this.update(_id, update); + } + + unsetAvatarOrigin(_id) { + const update = { + $unset: { + avatarOrigin: 1 + } + }; + + return this.update(_id, update); + } + + setUserActive(_id, active) { + if (active == null) { active = true; } + const update = { + $set: { + active + } + }; + + return this.update(_id, update); + } + + setAllUsersActive(active) { + const update = { + $set: { + active + } + }; + + return this.update({}, update, { multi: true }); + } + + unsetLoginTokens(_id) { + const update = { + $set: { + 'services.resume.loginTokens' : [] + } + }; + + return this.update(_id, update); + } + + unsetRequirePasswordChange(_id) { + const update = { + $unset: { + 'requirePasswordChange' : true, + 'requirePasswordChangeReason' : true + } + }; + + return this.update(_id, update); + } + + resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) { + const update = { + $unset: { + 'services.password': 1 + }, + $set: { + requirePasswordChange, + requirePasswordChangeReason + } + }; + + return this.update(_id, update); + } + + setLanguage(_id, language) { + const update = { + $set: { + language + } + }; + + return this.update(_id, update); + } + + setProfile(_id, profile) { + const update = { + $set: { + 'settings.profile': profile + } + }; + + return this.update(_id, update); + } + + setPreferences(_id, preferences) { + const update = { + $set: { + 'settings.preferences': preferences + } + }; + + return this.update(_id, update); + } + + setUtcOffset(_id, utcOffset) { + const query = { + _id, + utcOffset: { + $ne: utcOffset + } + }; + + const update = { + $set: { + utcOffset + } + }; + + return this.update(query, update); + } + + saveUserById(_id, data) { + const setData = {}; + const unsetData = {}; + + if (data.name != null) { + if (!_.isEmpty(s.trim(data.name))) { + setData.name = s.trim(data.name); + } else { + unsetData.name = 1; + } + } + + if (data.email != null) { + if (!_.isEmpty(s.trim(data.email))) { + setData.emails = [{address: s.trim(data.email)}]; + } else { + unsetData.emails = 1; + } + } + + if (data.phone != null) { + if (!_.isEmpty(s.trim(data.phone))) { + setData.phone = [{phoneNumber: s.trim(data.phone)}]; + } else { + unsetData.phone = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return true; + } + + return this.update({ _id }, update); + } + +// INSERT + create(data) { + const user = { + createdAt: new Date, + avatarOrigin: 'none' + }; + + _.extend(user, data); + + return this.insert(user); + } + + +// REMOVE + removeById(_id) { + return this.remove(_id); + } + +/* +Find users to send a message by email if: +- he is not online +- has a verified email +- has not disabled email notifications +- `active` is equal to true (false means they were deactivated and can't login) +*/ + getUsersToSendOfflineEmail(usersIds) { + const query = { + _id: { + $in: usersIds + }, + active: true, + status: 'offline', + statusConnection: { + $ne: 'online' + }, + 'emails.verified': true + }; + + return this.find(query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } }); + } +} + +RocketChat.models.Users = new ModelUsers(Meteor.users, true); -- GitLab From c25c1e6a2f83bd50676bd040f9ddb7199f47dbae Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 19 Apr 2017 17:34:11 -0300 Subject: [PATCH 013/280] convert webrtc still need to add the coments --- .../{ => client}/WebRTCClass.coffee | 4 +- .../rocketchat-webrtc/client/WebRTCClass.js | 1006 +++++++++++++++++ .../rocketchat-webrtc/{ => client}/adapter.js | 0 .../{ => client}/screenShare.coffee | 0 .../rocketchat-webrtc/client/screenShare.js | 31 + packages/rocketchat-webrtc/package.js | 14 +- packages/rocketchat-webrtc/server/settings.js | 22 + 7 files changed, 1069 insertions(+), 8 deletions(-) rename packages/rocketchat-webrtc/{ => client}/WebRTCClass.coffee (99%) create mode 100644 packages/rocketchat-webrtc/client/WebRTCClass.js rename packages/rocketchat-webrtc/{ => client}/adapter.js (100%) rename packages/rocketchat-webrtc/{ => client}/screenShare.coffee (100%) create mode 100644 packages/rocketchat-webrtc/client/screenShare.js create mode 100644 packages/rocketchat-webrtc/server/settings.js diff --git a/packages/rocketchat-webrtc/WebRTCClass.coffee b/packages/rocketchat-webrtc/client/WebRTCClass.coffee similarity index 99% rename from packages/rocketchat-webrtc/WebRTCClass.coffee rename to packages/rocketchat-webrtc/client/WebRTCClass.coffee index be46fdb70b..aefe207996 100644 --- a/packages/rocketchat-webrtc/WebRTCClass.coffee +++ b/packages/rocketchat-webrtc/client/WebRTCClass.coffee @@ -195,8 +195,10 @@ class WebRTCClass itemsById = {} for id, peerConnection of @peerConnections + console.log peerConnection for remoteStream in peerConnection.getRemoteStreams() - item = + console.log peerConnection.getRemoteStreams() + item = peerConnection.getRemoteStreams() id: id url: URL.createObjectURL(remoteStream) state: peerConnection.iceConnectionState diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.js b/packages/rocketchat-webrtc/client/WebRTCClass.js new file mode 100644 index 0000000000..57ef29c094 --- /dev/null +++ b/packages/rocketchat-webrtc/client/WebRTCClass.js @@ -0,0 +1,1006 @@ +/* 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) { + return console.log.apply(console, arguments); + } + } + + onUserStream(type, data) { + if (data.room !== this.webrtcInstance.room) { + return; + } + this.log('WebRTCTransportClass - onUser', type, data); + console.log(this.callbacks); + 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); + return 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) { + return RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'join', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } else { + return 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); + return 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); + return RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'description', data); + } + + sendStatus(data) { + this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room); + data.from = this.webrtcInstance.selfId; + return RocketChat.Notifications.notifyRoom(this.webrtcInstance.room, 'webrtc', 'status', data); + } + + onRemoteCall(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteCall'] == null) { + callbacks['onRemoteCall'] = []; + } + return callbacks['onRemoteCall'].push(fn); + } + + onRemoteJoin(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteJoin'] == null) { + callbacks['onRemoteJoin'] = []; + } + return callbacks['onRemoteJoin'].push(fn); + } + + onRemoteCandidate(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteCandidate'] == null) { + callbacks['onRemoteCandidate'] = []; + } + return callbacks['onRemoteCandidate'].push(fn); + } + + onRemoteDescription(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteDescription'] == null) { + callbacks['onRemoteDescription'] = []; + } + return callbacks['onRemoteDescription'].push(fn); + } + + onRemoteStatus(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteStatus'] == null) { + callbacks['onRemoteStatus'] = []; + } + return 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; + this.config.iceServers = []; + 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); + } + + log() { + if (this.debug === true) { + return console.log.apply(console, arguments); + } + } + + onError() { + return 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]; + console.log(peerConnection); + // const remoteStreams = peerConnection.getRemoteStreams(); + + peerConnection.getRemoteStreams().forEach((remoteStream) => { + const item = { + id, + url: URL.createObjectURL(remoteStream), + state: peerConnection.iceConnectionState + }; + console.log(item); + 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() { + return 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 + }); + }); + + return this.transport.sendStatus({ + media: this.media, + remoteConnections + }); + } + + + /* + @param data {Object} + from {String} + media {Object} + remoteConnections {Array[Object]} + id {String} + media {Object} + */ + + onRemoteStatus(data) { + 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', () => { + let ref; + if ((peerConnection.iceConnectionState === 'disconnected' || ref === '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; + } + return onSuccess(stream); + }; + return navigator.getUserMedia(media, onSuccessLocal, onError); + } + + getUserMedia(media, onSuccess, onError) { + if (onError == null) { + 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') { + chrome.webstore.install(undefined, refresh, function() { + window.open('https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf'); + refresh(); + }); + } 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) { + return getScreen(); + } else { + const getAudioSuccess = (audioStream) => { + return 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); + }); + return callback(null, this.localStream); + }; + const onError = (error) => { + callback(false); + return this.onError(error); + }; + return 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) { + if (enabled == null) { + enabled = true; + } + if (this.localStream != null) { + if (enabled === true && this.media.audio !== true) { + delete this.localStream; + this.media.audio = true; + return this.getLocalUserMedia(() => { + this.stopAllPeerConnections(); + return this.joinCall(); + }); + } else { + this.localStream.getAudioTracks().forEach(function(audio) { + return audio.enabled = enabled; + }); + return this.audioEnabled.set(enabled); + } + } + } + + disableAudio() { + return this.setAudioEnabled(false); + } + + enableAudio() { + return this.setAudioEnabled(true); + } + + setVideoEnabled(enabled) { + if (enabled == null) { + enabled = true; + } + if (this.localStream != null) { + if (enabled === true && this.media.video !== true) { + delete this.localStream; + this.media.video = true; + return this.getLocalUserMedia(() => { + this.stopAllPeerConnections(); + return this.joinCall(); + }); + } else { + this.localStream.getVideoTracks().forEach(function(video) { + return video.enabled = enabled; + }); + return this.videoEnabled.set(enabled); + } + } + } + + disableScreenShare() { + return this.setScreenShareEnabled(false); + } + + enableScreenShare() { + return this.setScreenShareEnabled(true); + } + + setScreenShareEnabled(enabled) { + if (enabled == null) { + enabled = true; + } + if (this.localStream != null) { + this.media.desktop = enabled; + delete this.localStream; + return this.getLocalUserMedia((err) => { + if (err != null) { + return; + } + this.screenShareEnabled.set(enabled); + this.stopAllPeerConnections(); + return this.joinCall(); + }); + } + } + + disableVideo() { + return this.setVideoEnabled(false); + } + + enableVideo() { + return 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(function(track) { + return track.stop(); + }); + } + this.localUrl.set(undefined); + delete this.localStream; + return this.stopAllPeerConnections(); + } + + + /* + @param media {Object} + audio {Boolean} + video {Boolean} + */ + + startCall(media) { + if (media == null) { + media = {}; + } + this.log('startCall', arguments); + this.media = media; + return this.getLocalUserMedia(() => { + this.active = true; + return this.transport.startCall({ + media: this.media + }); + }); + } + + startCallAsMonitor(media) { + if (media == null) { + media = {}; + } + this.log('startCallAsMonitor', arguments); + this.media = media; + this.active = true; + this.monitor = true; + return 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(() => { + return 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 }`; + } + return swal({ + title: `${ title }`, + text: 'Do you want to accept?', + html: true, + showCancelButton: true, + confirmButtonText: 'Yes', + cancelButtonText: 'No' + }, (isConfirm) => { + if (isConfirm) { + FlowRouter.goToRoomById(data.room); + return Meteor.defer(() => { + return this.joinCall({ + to: data.from, + monitor: data.monitor, + media: data.media + }); + }); + } else { + return this.stop(); + } + }); + } + + + /* + @param data {Object} + to {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + joinCall(data) { + if (data == null) { + 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); + 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 = () => { + return this.transport.sendDescription({ + to: data.from, + type: 'offer', + ts: peerConnection.createdAt, + media: this.media, + description: { + sdp: offer.sdp, + type: offer.type + } + }); + }; + + return peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); + }; + + if (data.monitor === true) { + return peerConnection.createOffer(onOffer, this.onError, { + mandatory: { + OfferToReceiveAudio: data.media.audio, + OfferToReceiveVideo: data.media.video + } + }); + } else { + return 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 = () => { + return this.transport.sendDescription({ + to: data.from, + type: 'answer', + ts: peerConnection.createdAt, + description: { + sdp: answer.sdp, + type: answer.type + } + }); + }; + + return peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); + }; + + return 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') { + return 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; + return this.onRemoteOffer({ + from: data.from, + ts: data.ts, + description: data.description + }); + } else { + return 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() { + return Tracker.autorun(function() { + if (Meteor.userId()) { + return RocketChat.Notifications.onUser('webrtc', (type, data) => { + if (data.room == null) { + return; + } + const webrtc = WebRTC.getInstanceByRoomId(data.room); + return 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/screenShare.coffee b/packages/rocketchat-webrtc/client/screenShare.coffee similarity index 100% rename from packages/rocketchat-webrtc/screenShare.coffee rename to packages/rocketchat-webrtc/client/screenShare.coffee diff --git a/packages/rocketchat-webrtc/client/screenShare.js b/packages/rocketchat-webrtc/client/screenShare.js new file mode 100644 index 0000000000..f4ff0eef4d --- /dev/null +++ b/packages/rocketchat-webrtc/client/screenShare.js @@ -0,0 +1,31 @@ +/* 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', '*'); + } else { + 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'); + } else { + throw new Error('PermissionDeniedError'); + } + } + if (e.data.sourceId != null) { + return typeof ChromeScreenShare.screenCallback === 'function' ? ChromeScreenShare.screenCallback(e.data.sourceId) : void 0; + } +}); diff --git a/packages/rocketchat-webrtc/package.js b/packages/rocketchat-webrtc/package.js index 53d56d1cde..66c3258e0e 100644 --- a/packages/rocketchat-webrtc/package.js +++ b/packages/rocketchat-webrtc/package.js @@ -7,16 +7,16 @@ Package.describe({ Package.onUse(function(api) { api.use('rocketchat:lib'); - api.use('coffeescript'); api.use('ecmascript'); + api.use('coffeescript'); 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.coffee', 'server'); + api.addFiles('server/settings.js', 'server'); - api.export('WebRTC'); + api.export('WebRTC', 'client'); }); diff --git a/packages/rocketchat-webrtc/server/settings.js b/packages/rocketchat-webrtc/server/settings.js new file mode 100644 index 0000000000..c32f3103ac --- /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 + }); +}); -- GitLab From 0a4c04d14eb56487c85504e6be5b547d24b5044d Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 20 Apr 2017 17:25:37 -0300 Subject: [PATCH 014/280] Add S3 storage type to avatars --- .../client/lib/FileUploadAmazonS3.js | 33 +-- .../client/lib/fileUploadHandler.js | 4 +- .../globalFileRestrictions.js | 1 + .../server/config/configFileUploadAmazonS3.js | 148 ++++++----- .../server/lib/FileUpload.js | 2 +- .../server/models/Uploads.coffee | 53 +++- .../client/avatar/prompt.coffee | 34 ++- .../rocketchat-ui/client/lib/fileUpload.js | 232 ++++++++++++++++++ packages/rocketchat-ui/package.js | 4 +- server/methods/setAvatarFromService.js | 7 + server/startup/avatar.js | 147 ++++++++--- 11 files changed, 531 insertions(+), 134 deletions(-) create mode 100644 packages/rocketchat-ui/client/lib/fileUpload.js diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js index 277f6471a9..df9fa58e3f 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js @@ -1,36 +1,19 @@ /* globals FileUpload, FileUploadBase, Slingshot */ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { - constructor(meta, file) { + constructor(directive, meta, file) { super(meta, file); - this.uploader = new Slingshot.Upload('rocketchat-uploads', { rid: meta.rid }); + this.uploader = new Slingshot.Upload(directive, meta); } - start() { + start(callback) { this.uploader.send(this.file, (error, downloadUrl) => { if (this.computation) { this.computation.stop(); } if (error) { - let uploading = Session.get('uploading'); - if (!Array.isArray(uploading)) { - uploading = []; - } - - const item = _.findWhere(uploading, { id: this.id }); - - if (_.isObject(item)) { - item.error = error.error; - item.percentage = 0; - } else { - uploading.push({ - error: error.error, - percentage: 0 - }); - } - - Session.set('uploading', uploading); + return callback.call(this, error); } else { const file = _.pick(this.meta, 'type', 'size', 'name', 'identify', 'description'); file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); @@ -38,13 +21,7 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { Meteor.call('sendFileMessage', this.meta.rid, 's3', file, () => { Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } + callback.call(this, null, file); }, 2000); }); } diff --git a/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js b/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js index 71f5fbdf15..f62e4bb5b9 100644 --- a/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js +++ b/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js @@ -1,10 +1,10 @@ /* globals FileUpload, fileUploadHandler:true */ /* exported fileUploadHandler */ -fileUploadHandler = (meta, file) => { +fileUploadHandler = (directive, meta, file) => { const storageType = RocketChat.settings.get('FileUpload_Storage_Type'); if (FileUpload[storageType] !== undefined) { - return new FileUpload[storageType](meta, file); + return new FileUpload[storageType](directive, meta, file); } }; diff --git a/packages/rocketchat-file-upload/globalFileRestrictions.js b/packages/rocketchat-file-upload/globalFileRestrictions.js index 0568c018d8..3c52390c43 100644 --- a/packages/rocketchat-file-upload/globalFileRestrictions.js +++ b/packages/rocketchat-file-upload/globalFileRestrictions.js @@ -25,5 +25,6 @@ const slingShotConfig = { allowedFileTypes: null }; +Slingshot.fileRestrictions('rocketchat-avatars', slingShotConfig); Slingshot.fileRestrictions('rocketchat-uploads', slingShotConfig); Slingshot.fileRestrictions('rocketchat-uploads-gs', slingShotConfig); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index daac0ea199..284edb1992 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -1,4 +1,4 @@ -/* globals Slingshot, FileUpload, AWS, SystemLogger */ +/* globals Slingshot, FileUpload, AWS */ const crypto = Npm.require('crypto'); let S3accessKey; @@ -36,8 +36,81 @@ FileUpload.addHandler('s3', { } }); -const createS3Directive = _.debounce(() => { - const directiveName = 'rocketchat-uploads'; +function createDirective(directiveName, { key, bucket, accessKey, secretKey, region, acl, cdn, bucketUrl}) { + if (Slingshot._directives[directiveName]) { + delete Slingshot._directives[directiveName]; + } + const config = { + bucket, + key, + AWSAccessKeyId: accessKey, + AWSSecretAccessKey: secretKey + }; + + if (!_.isEmpty(acl)) { + config.acl = acl; + } + + if (!_.isEmpty(cdn)) { + config.cdn = cdn; + } + + if (!_.isEmpty(region)) { + config.region = region; + } + + if (!_.isEmpty(bucketUrl)) { + config.bucketUrl = bucketUrl; + } + + try { + Slingshot.createDirective(directiveName, Slingshot.S3Storage, config); + } catch (e) { + console.error('Error configuring S3 ->', e.message); + } +} + +const configureSlingshot = _.debounce(() => { + const directives = [ + { + name: 'rocketchat-uploads', + key(file, metaContext) { + const path = `${ RocketChat.hostname }/${ metaContext.rid }/${ this.userId }/`; + + const upload = { + rid: metaContext.rid, + s3: { + bucket: RocketChat.settings.get('FileUpload_S3_Bucket'), + region: RocketChat.settings.get('FileUpload_S3_Region'), + path + } + }; + const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 's3', file, upload); + + return path + fileId; + } + }, + { + name: 'rocketchat-avatars', + key(file/*, metaContext*/) { + const path = `${ RocketChat.hostname }/avatars/`; + + const user = RocketChat.models.Users.findOneById(this.userId); + + const upload = { + username: user && user.username, + s3: { + bucket: RocketChat.settings.get('FileUpload_S3_Bucket'), + region: RocketChat.settings.get('FileUpload_S3_Region'), + path + } + }; + RocketChat.models.Uploads.insertFileInitByUsername(user.username, this.userId, 's3', file, upload); + + return path + user.username; + } + } + ]; const type = RocketChat.settings.get('FileUpload_Storage_Type'); const bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); @@ -54,76 +127,41 @@ const createS3Directive = _.debounce(() => { }); if (type === 'AmazonS3' && !_.isEmpty(bucket) && !_.isEmpty(accessKey) && !_.isEmpty(secretKey)) { - if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } - const config = { - bucket, - AWSAccessKeyId: accessKey, - AWSSecretAccessKey: secretKey, - key(file, metaContext) { - const path = `${ RocketChat.hostname }/${ metaContext.rid }/${ this.userId }/`; - - const upload = { s3: { - bucket, - region, - path - }}; - const fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 's3', file, upload); - - return path + fileId; + directives.forEach((conf) => { + createDirective(conf.name, { key: conf.key, bucket, accessKey, secretKey, region, acl, cdn, bucketUrl}); + }); + } else { + directives.forEach((conf) => { + if (Slingshot._directives[conf.name]) { + delete Slingshot._directives[conf.name]; } - }; - - if (!_.isEmpty(acl)) { - config.acl = acl; - } - - if (!_.isEmpty(cdn)) { - config.cdn = cdn; - } - - if (!_.isEmpty(region)) { - config.region = region; - } - - if (!_.isEmpty(bucketUrl)) { - config.bucketUrl = bucketUrl; - } - - try { - Slingshot.createDirective(directiveName, Slingshot.S3Storage, config); - } catch (e) { - SystemLogger.error('Error configuring S3 ->', e.message); - } - } else if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; + }); } }, 500); -RocketChat.settings.get('FileUpload_Storage_Type', createS3Directive); +RocketChat.settings.get('FileUpload_Storage_Type', configureSlingshot); -RocketChat.settings.get('FileUpload_S3_Bucket', createS3Directive); +RocketChat.settings.get('FileUpload_S3_Bucket', configureSlingshot); -RocketChat.settings.get('FileUpload_S3_Acl', createS3Directive); +RocketChat.settings.get('FileUpload_S3_Acl', configureSlingshot); RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId', function(key, value) { S3accessKey = value; - createS3Directive(); + configureSlingshot(); }); RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey', function(key, value) { S3secretKey = value; - createS3Directive(); + configureSlingshot(); }); RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan', function(key, value) { S3expiryTimeSpan = value; - createS3Directive(); + configureSlingshot(); }); -RocketChat.settings.get('FileUpload_S3_CDN', createS3Directive); +RocketChat.settings.get('FileUpload_S3_CDN', configureSlingshot); -RocketChat.settings.get('FileUpload_S3_Region', createS3Directive); +RocketChat.settings.get('FileUpload_S3_Region', configureSlingshot); -RocketChat.settings.get('FileUpload_S3_BucketURL', createS3Directive); +RocketChat.settings.get('FileUpload_S3_BucketURL', configureSlingshot); diff --git a/packages/rocketchat-file-upload/server/lib/FileUpload.js b/packages/rocketchat-file-upload/server/lib/FileUpload.js index cb0aadbc06..05ec504af3 100644 --- a/packages/rocketchat-file-upload/server/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/server/lib/FileUpload.js @@ -16,7 +16,7 @@ FileUpload.delete = function(fileId) { this.handlers[file.store].delete(file); - return RocketChat.models.Uploads.remove(file._id); + return RocketChat.models.Uploads.deleteFile(file._id); }; FileUpload.get = function(file, req, res, next) { diff --git a/packages/rocketchat-lib/server/models/Uploads.coffee b/packages/rocketchat-lib/server/models/Uploads.coffee index 53dd792568..2604403bbe 100644 --- a/packages/rocketchat-lib/server/models/Uploads.coffee +++ b/packages/rocketchat-lib/server/models/Uploads.coffee @@ -29,9 +29,8 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return @find fileQuery, fileOptions - insertFileInit: (roomId, userId, store, file, extra) -> + insertFileInit: (userId, store, file, extra) -> fileData = - rid: roomId userId: userId store: store complete: false @@ -49,6 +48,23 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return file + insertFileInitByUsername: (username, userId, store, file, extra) -> + fileData = + _id: username + userId: userId + store: store + complete: false + uploading: true + progress: 0 + extension: s.strRightBack(file.name, '.') + uploadedAt: new Date() + + _.extend(fileData, file, extra); + + file = @insertOrUpsert fileData + + return file + updateFileComplete: (fileId, userId, file) -> if not fileId return @@ -65,9 +81,40 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base update.$set = _.extend file, update.$set - if @model.direct?.insert? + if @model.direct?.update? + result = @model.direct.update filter, update + else + result = @update filter, update + + return result + + updateFileCompleteByUsername: (username, userId, url) -> + if not username + return + + filter = + username: username + userId: userId + + update = + $set: + complete: true + uploading: false + progress: 1 + url: url + + if @model.direct?.update? result = @model.direct.update filter, update else result = @update filter, update return result + + findOneByUsername: (username) -> + return @findOne username: username + + deleteFile: (fileId) -> + if @model.direct?.remove? + return @model.direct.remove { _id: fileId } + else + return @remove { _id: fileId } diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.coffee b/packages/rocketchat-ui-account/client/avatar/prompt.coffee index 50f3d8838f..5b4849a1dc 100644 --- a/packages/rocketchat-ui-account/client/avatar/prompt.coffee +++ b/packages/rocketchat-ui-account/client/avatar/prompt.coffee @@ -1,4 +1,6 @@ import toastr from 'toastr' +import mime from 'mime-type/with-db' + Template.avatarPrompt.onCreated -> self = this self.suggestions = new ReactiveVar @@ -38,7 +40,7 @@ Template.avatarPrompt.helpers return '@'+Meteor.user()?.username Template.avatarPrompt.events - 'click .select-service': -> + 'click .select-service': (event, instance) -> if @service is 'initials' Meteor.call 'resetAvatar', (err) -> if err?.details?.timeToReset? @@ -60,13 +62,29 @@ Template.avatarPrompt.events 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) + files = instance.find('input[type=file]').files + if not files or files.length is 0 + files = e.dataTransfer?.files or [] + + for file in files + Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }) + + record = + name: files[0].name + size: files[0].size + type: files[0].type + # description: document.getElementById('file-description').value + + upload = fileUploadHandler 'rocketchat-avatars', record, files[0] + + # upload.onProgress = (progress) -> + # console.log 'progress ->', progress + + upload.start (error, result) -> + if result + Meteor.call 'saveAvatarFile', result, () => + toastr.success t('Avatar_changed_successfully') + RocketChat.callbacks.run('userAvatarSet', @service) 'click .login-with-service': (event, template) -> loginWithService = "loginWith#{_.capitalize(this)}" diff --git a/packages/rocketchat-ui/client/lib/fileUpload.js b/packages/rocketchat-ui/client/lib/fileUpload.js new file mode 100644 index 0000000000..ad91b1fc40 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/fileUpload.js @@ -0,0 +1,232 @@ +/* globals fileUploadHandler, Handlebars, fileUpload */ +/* exported fileUpload */ + +function readAsDataURL(file, callback) { + const reader = new FileReader(); + reader.onload = ev => callback(ev.target.result, file); + + return reader.readAsDataURL(file); +} + +function getUploadPreview(file, callback) { + // If greater then 10MB don't try and show a preview + if (file.file.size > (10 * 1000000)) { + return callback(file, null); + } else if ((file.file.type.indexOf('audio') > -1) || (file.file.type.indexOf('video') > -1) || (file.file.type.indexOf('image') > -1)) { + file.type = file.file.type.split('/')[0]; + + return readAsDataURL(file.file, content => callback(file, content)); + } else { + return callback(file, null); + } +} + +function formatBytes(bytes, decimals) { + if (bytes === 0) { + return '0 Bytes'; + } + + const k = 1000; + const dm = (decimals + 1) || 3; + + const sizes = [ + 'Bytes', + 'KB', + 'MB', + 'GB', + 'TB', + 'PB' + ]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${ parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) } ${ sizes[i] }`; +} + +fileUpload = function(filesToUpload) { + const roomId = Session.get('openedRoom'); + const files = [].concat(filesToUpload); + + function consume() { + const file = files.pop(); + if ((file == null)) { + swal.close(); + return; + } + + if (!RocketChat.fileUploadIsValidContentType(file.file.type)) { + swal({ + title: t('FileUpload_MediaType_NotAccepted'), + text: file.file.type || `*.${ s.strRightBack(file.file.name, '.') }`, + type: 'error', + timer: 3000 + }); + return; + } + + if (file.file.size === 0) { + swal({ + title: t('FileUpload_File_Empty'), + type: 'error', + timer: 1000 + }); + return; + } + + return getUploadPreview(file, function(file, preview) { + let text = ''; + + if (file.type === 'audio') { + text = `\ +
+ +
+
+ + +
`; + } else if (file.type === 'video') { + text = `\ +
+ +
+
+ + +
`; + } else if (file.type === 'image') { + text = `\ +
+
+
+
+ + +
`; + } else { + const fileSize = formatBytes(file.file.size); + + text = `\ +
+
${ Handlebars._escape(file.name) } - ${ fileSize }
+
+
+ + +
`; + } + + return swal({ + title: t('Upload_file_question'), + text, + showCancelButton: true, + closeOnConfirm: false, + closeOnCancel: false, + confirmButtonText: t('Send'), + cancelButtonText: t('Cancel'), + html: true + }, function(isConfirm) { + consume(); + if (isConfirm !== true) { + return; + } + + const record = { + name: document.getElementById('file-name').value || file.name || file.file.name, + size: file.file.size, + type: file.file.type, + rid: roomId, + description: document.getElementById('file-description').value + }; + + const upload = fileUploadHandler('rocketchat-uploads', record, file.file); + + let uploading = Session.get('uploading') || []; + uploading.push({ + id: upload.id, + name: upload.getFileName(), + percentage: 0 + }); + + Session.set('uploading', uploading); + + upload.onProgress = function(progress) { + uploading = Session.get('uploading'); + + const item = _.findWhere(uploading, {id: upload.id}); + if (item != null) { + item.percentage = Math.round(progress * 100) || 0; + return Session.set('uploading', uploading); + } + }; + + upload.start(function(error, file) { + if (error) { + let uploading = Session.get('uploading'); + if (!Array.isArray(uploading)) { + uploading = []; + } + + const item = _.findWhere(uploading, { id: this.id }); + + if (_.isObject(item)) { + item.error = error.error; + item.percentage = 0; + } else { + uploading.push({ + error: error.error, + percentage: 0 + }); + } + + Session.set('uploading', uploading); + return; + } else if (file) { + const uploading = Session.get('uploading'); + if (uploading !== null) { + const item = _.findWhere(uploading, { + id: this.id + }); + return Session.set('uploading', _.without(uploading, item)); + } + } + }); + + Tracker.autorun(function(c) { + const cancel = Session.get(`uploading-cancel-${ upload.id }`); + if (cancel) { + let item; + upload.stop(); + c.stop(); + + uploading = Session.get('uploading'); + if (uploading != null) { + item = _.findWhere(uploading, {id: upload.id}); + if (item != null) { + item.percentage = 0; + } + Session.set('uploading', uploading); + } + + return Meteor.setTimeout(function() { + uploading = Session.get('uploading'); + if (uploading != null) { + item = _.findWhere(uploading, {id: upload.id}); + return Session.set('uploading', _.without(uploading, item)); + } + } + , 1000); + } + }); + }); + }); + } + + consume(); +}; diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index 0a75485530..acd455f1fe 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -38,7 +38,7 @@ Package.onUse(function(api) { api.addFiles('client/lib/chatMessages.coffee', 'client'); api.addFiles('client/lib/collections.js', 'client'); api.addFiles('client/lib/customEventPolyfill.js', 'client'); - api.addFiles('client/lib/fileUpload.coffee', 'client'); + api.addFiles('client/lib/fileUpload.js', 'client'); api.addFiles('client/lib/fireEvent.js', 'client'); api.addFiles('client/lib/iframeCommands.js', 'client'); api.addFiles('client/lib/menu.js', 'client'); @@ -108,4 +108,6 @@ Package.onUse(function(api) { api.addFiles('client/views/app/videoCall/videoButtons.js', 'client'); api.addFiles('client/views/app/videoCall/videoCall.js', 'client'); api.addFiles('client/views/app/photoswipe.js', 'client'); + + api.export('fileUpload'); }); diff --git a/server/methods/setAvatarFromService.js b/server/methods/setAvatarFromService.js index 6dc13b719e..bdc0768552 100644 --- a/server/methods/setAvatarFromService.js +++ b/server/methods/setAvatarFromService.js @@ -19,6 +19,13 @@ Meteor.methods({ const user = Meteor.user(); return RocketChat.setUserAvatar(user, dataURI, contentType, service); + }, + + saveAvatarFile(file) { + const user = RocketChat.models.Users.findOneById(Meteor.userId()); + // console.log('user ->', user); + // console.log('file ->', file); + RocketChat.models.Uploads.updateFileCompleteByUsername(user.username, Meteor.userId(), file.url); } }); diff --git a/server/startup/avatar.js b/server/startup/avatar.js index 6aca22c665..634effe061 100644 --- a/server/startup/avatar.js +++ b/server/startup/avatar.js @@ -35,13 +35,11 @@ Meteor.startup(function() { transformWrite }); - return WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/*, next*/) { + WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/*, next*/) { const params = { username: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) }; - let username = params.username.replace(/\.jpg$/, '').replace(/^@/, ''); - if (_.isEmpty(params.username)) { res.writeHead(403); res.write('Forbidden'); @@ -49,24 +47,23 @@ Meteor.startup(function() { return; } - let file; + return getNewAvatar(params, req, res); + })); +}); - if (params.username[0] !== '@') { - if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm) { - const user = RocketChat.models.Users.findOneByUsername(username); - if (user && user.services && user.services.sandstorm && user.services.sandstorm.picture) { - res.setHeader('Location', user.services.sandstorm.picture); - res.writeHead(302); - res.end(); - return; - } - } - file = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ username }.jpg`)); - } +function getNewAvatar(params, req, res) { + const match = /^\/([^?]*)/.exec(req.url); + + if (match[1]) { + let username = decodeURIComponent(match[1]); + const file = RocketChat.models.Uploads.findOneByUsername(username); + + if (file) { + res.setHeader('Content-Security-Policy', 'default-src \'none\''); - res.setHeader('Content-Disposition', 'inline'); - if (!file) { + return FileUpload.get(file, req, res); + } else { res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'public, max-age=0'); res.setHeader('Expires', '-1'); @@ -120,35 +117,113 @@ Meteor.startup(function() { return; } + } + + res.writeHead(404); + res.end(); + return; +} + +function getOldAvatar(params, req, res) { + let username = params.username.replace(/\.jpg$/, '').replace(/^@/, ''); + let file; + if (params.username[0] !== '@') { + if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm) { + const user = RocketChat.models.Users.findOneByUsername(username); + if (user && user.services && user.services.sandstorm && user.services.sandstorm.picture) { + res.setHeader('Location', user.services.sandstorm.picture); + res.writeHead(302); + res.end(); + return; + } + } + + file = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ username }.jpg`)); + } + + res.setHeader('Content-Disposition', 'inline'); + if (!file) { + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { - if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { - res.setHeader('Last-Modified', reqModifiedHeader); + if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { res.writeHead(304); res.end(); return; } } - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); - res.setHeader('Content-Length', file.length); + const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; - if (file.contentType) { - res.setHeader('Content-Type', file.contentType); - } else { - file.readStream.once('data', function(chunk) { - const fileType = getFileType(chunk); - if (fileType) { - return res.setHeader('Content-Type', fileType.mime); - } else { - return res.setHeader('Content-Type', 'image/jpeg'); + if (RocketChat.settings.get('UI_Use_Name_Avatar')) { + const user = RocketChat.models.Users.findOneByUsername(username, { + fields: { + name: 1 } }); + + if (user && user.name) { + username = user.name; + } } - file.readStream.pipe(res); - })); -}); + let color = ''; + let initials = ''; + + if (username === '?') { + color = '#000'; + initials = username; + } else { + const position = username.length % colors.length; + + color = colors[position]; + username = username.replace(/[^A-Za-z0-9]/g, '.').replace(/\.+/g, '.').replace(/(^\.)|(\.$)/g, ''); + + const usernameParts = username.split('.'); + + initials = usernameParts.length > 1 ? _.first(usernameParts)[0] + _.last(usernameParts)[0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); + initials = initials.toUpperCase(); + } + + const svg = `\n\n \n ${ initials }\n \n`; + res.write(svg); + res.end(); + + return; + } + + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); + res.setHeader('Content-Length', file.length); + + if (file.contentType) { + res.setHeader('Content-Type', file.contentType); + } else { + file.readStream.once('data', function(chunk) { + const fileType = getFileType(chunk); + if (fileType) { + return res.setHeader('Content-Type', fileType.mime); + } else { + return res.setHeader('Content-Type', 'image/jpeg'); + } + }); + } + + file.readStream.pipe(res); +} -- GitLab From 481e0fa5638bad0daa24f6764287f5191d574ad0 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 24 Apr 2017 11:03:52 -0300 Subject: [PATCH 015/280] Remove Console logs --- .../client/WebRTCClass.coffee | 3 +-- .../rocketchat-webrtc/client/WebRTCClass.js | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.coffee b/packages/rocketchat-webrtc/client/WebRTCClass.coffee index aefe207996..ffefcc9154 100644 --- a/packages/rocketchat-webrtc/client/WebRTCClass.coffee +++ b/packages/rocketchat-webrtc/client/WebRTCClass.coffee @@ -197,8 +197,7 @@ class WebRTCClass for id, peerConnection of @peerConnections console.log peerConnection for remoteStream in peerConnection.getRemoteStreams() - console.log peerConnection.getRemoteStreams() - item = peerConnection.getRemoteStreams() + item = id: id url: URL.createObjectURL(remoteStream) state: peerConnection.iceConnectionState diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.js b/packages/rocketchat-webrtc/client/WebRTCClass.js index 57ef29c094..decfab47d5 100644 --- a/packages/rocketchat-webrtc/client/WebRTCClass.js +++ b/packages/rocketchat-webrtc/client/WebRTCClass.js @@ -27,7 +27,6 @@ class WebRTCTransportClass { return; } this.log('WebRTCTransportClass - onUser', type, data); - console.log(this.callbacks); const onRemoteCall = this.callbacks['onRemoteCall']; const onRemoteJoin = this.callbacks['onRemoteJoin']; const onRemoteCandidate = this.callbacks['onRemoteCandidate']; @@ -222,6 +221,8 @@ class WebRTCClass { 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() { @@ -251,8 +252,6 @@ class WebRTCClass { Object.keys(peerConnections).forEach((id) => { const peerConnection = peerConnections[id]; - console.log(peerConnection); - // const remoteStreams = peerConnection.getRemoteStreams(); peerConnection.getRemoteStreams().forEach((remoteStream) => { const item = { @@ -322,6 +321,7 @@ class WebRTCClass { */ onRemoteStatus(data) { + //this.log(onRemoteStatus, arguments); this.callInProgress.set(true); Meteor.clearTimeout(this.callInProgressTimeout); this.callInProgressTimeout = Meteor.setTimeout(this.resetCallInProgress.bind(this), 2000); @@ -811,6 +811,15 @@ class WebRTCClass { } 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); @@ -854,7 +863,9 @@ class WebRTCClass { onRemoteOffer(data) { - if (this.active !== true) { return; } + if (this.active !== true) { + return; + } this.log('onRemoteOffer', arguments); let peerConnection = this.getPeerConnection(data.from); -- GitLab From 7ef71c6ea05f45973f131ff58d35afc82ab68af0 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 24 Apr 2017 11:09:42 -0300 Subject: [PATCH 016/280] Remove Coffee Files --- .../client/WebRTCClass.coffee | 819 ------------------ .../client/screenShare.coffee | 27 - packages/rocketchat-webrtc/package.js | 1 - .../rocketchat-webrtc/server/settings.coffee | 5 - 4 files changed, 852 deletions(-) delete mode 100644 packages/rocketchat-webrtc/client/WebRTCClass.coffee delete mode 100644 packages/rocketchat-webrtc/client/screenShare.coffee delete mode 100644 packages/rocketchat-webrtc/server/settings.coffee diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.coffee b/packages/rocketchat-webrtc/client/WebRTCClass.coffee deleted file mode 100644 index ffefcc9154..0000000000 --- a/packages/rocketchat-webrtc/client/WebRTCClass.coffee +++ /dev/null @@ -1,819 +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 - console.log peerConnection - 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' - chrome.webstore.install undefined, refresh, -> - window.open('https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf') - 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: "#{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/screenShare.coffee b/packages/rocketchat-webrtc/client/screenShare.coffee deleted file mode 100644 index c157e7c045..0000000000 --- a/packages/rocketchat-webrtc/client/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/package.js b/packages/rocketchat-webrtc/package.js index 66c3258e0e..608f3d9176 100644 --- a/packages/rocketchat-webrtc/package.js +++ b/packages/rocketchat-webrtc/package.js @@ -8,7 +8,6 @@ Package.describe({ Package.onUse(function(api) { api.use('rocketchat:lib'); api.use('ecmascript'); - api.use('coffeescript'); api.use('templating', 'client'); api.mainModule('client/WebRTCClass.js', 'client'); diff --git a/packages/rocketchat-webrtc/server/settings.coffee b/packages/rocketchat-webrtc/server/settings.coffee deleted file mode 100644 index 84337ffbc4..0000000000 --- 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} -- GitLab From c9eb3a250553cb1987d06e598c95ea1aa6a420a3 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 24 Apr 2017 11:18:25 -0300 Subject: [PATCH 017/280] fix EsLint --- packages/rocketchat-webrtc/client/screenShare.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-webrtc/client/screenShare.js b/packages/rocketchat-webrtc/client/screenShare.js index f4ff0eef4d..aab43e8ceb 100644 --- a/packages/rocketchat-webrtc/client/screenShare.js +++ b/packages/rocketchat-webrtc/client/screenShare.js @@ -26,6 +26,6 @@ window.addEventListener('message', function(e) { } } if (e.data.sourceId != null) { - return typeof ChromeScreenShare.screenCallback === 'function' ? ChromeScreenShare.screenCallback(e.data.sourceId) : void 0; + return typeof ChromeScreenShare.screenCallback === 'function' && ChromeScreenShare.screenCallback(e.data.sourceId); } }); -- GitLab From a2de9f51557ba1a522a5026a3e98e5d8e5c8c55f Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Apr 2017 09:38:03 -0300 Subject: [PATCH 018/280] Add Google Cloud Storage support to avatars --- .../client/lib/FileUploadGoogleStorage.js | 33 ++----- .../globalFileRestrictions.js | 1 + .../config/configFileUploadGoogleStorage.js | 88 ++++++++++++++----- .../client/avatar/prompt.coffee | 2 +- .../rocketchat-ui/client/lib/fileUpload.js | 2 +- 5 files changed, 73 insertions(+), 53 deletions(-) diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js index d1daa62ec0..b1db2b767d 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js @@ -1,36 +1,19 @@ /* globals FileUpload, FileUploadBase, Slingshot */ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileUploadBase { - constructor(meta, file) { + constructor(directive, meta, file) { super(meta, file); - this.uploader = new Slingshot.Upload('rocketchat-uploads-gs', { rid: meta.rid }); + this.uploader = new Slingshot.Upload(directive, { rid: meta.rid }); } - start() { + start(callback) { this.uploader.send(this.file, (error, downloadUrl) => { if (this.computation) { this.computation.stop(); } if (error) { - let uploading = Session.get('uploading'); - if (!Array.isArray(uploading)) { - uploading = []; - } - - const item = _.findWhere(uploading, { id: this.id }); - - if (_.isObject(item)) { - item.error = error.error; - item.percentage = 0; - } else { - uploading.push({ - error: error.error, - percentage: 0 - }); - } - - Session.set('uploading', uploading); + return callback.call(this, error); } else { const file = _.pick(this.meta, 'type', 'size', 'name', 'identify', 'description'); file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); @@ -38,13 +21,7 @@ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileU Meteor.call('sendFileMessage', this.meta.rid, 'googleCloudStorage', file, () => { Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } + callback.call(this, null, file); }, 2000); }); } diff --git a/packages/rocketchat-file-upload/globalFileRestrictions.js b/packages/rocketchat-file-upload/globalFileRestrictions.js index 3c52390c43..8e2d5086de 100644 --- a/packages/rocketchat-file-upload/globalFileRestrictions.js +++ b/packages/rocketchat-file-upload/globalFileRestrictions.js @@ -28,3 +28,4 @@ const slingShotConfig = { Slingshot.fileRestrictions('rocketchat-avatars', slingShotConfig); Slingshot.fileRestrictions('rocketchat-uploads', slingShotConfig); Slingshot.fileRestrictions('rocketchat-uploads-gs', slingShotConfig); +Slingshot.fileRestrictions('rocketchat-avatars-gs', slingShotConfig); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js index 9e8d5219ba..b7ae3dfa45 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js @@ -43,6 +43,25 @@ function generateDeleteUrl({ file }) { return `https://${ file.googleCloudStorage.bucket }.storage.googleapis.com/${ encodeURIComponent(parts.path) }?GoogleAccessId=${ parts.accessId }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; } +function createDirective(directiveName, { key, bucket, accessId, secret }) { + if (Slingshot._directives[directiveName]) { + delete Slingshot._directives[directiveName]; + } + + const config = { + bucket, + GoogleAccessId: accessId, + GoogleSecretKey: secret, + key + }; + + try { + Slingshot.createDirective(directiveName, Slingshot.GoogleCloud, config); + } catch (e) { + console.error('Error configuring GoogleCloudStorage ->', e.message); + } +} + FileUpload.addHandler('googleCloudStorage', { get(file, req, res) { const fileUrl = generateGetURL({ file }); @@ -71,7 +90,43 @@ FileUpload.addHandler('googleCloudStorage', { }); const createGoogleStorageDirective = _.debounce(() => { - const directiveName = 'rocketchat-uploads-gs'; + const directives = [ + { + name: 'rocketchat-uploads-gs', + key: function _googleCloudStorageKey(file, metaContext) { + const path = `${ RocketChat.settings.get('uniqueID') }/${ metaContext.rid }/${ this.userId }/`; + const upload = { + rid: metaContext.rid, + googleCloudStorage: { + bucket: RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'), + path + } + }; + const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 'googleCloudStorage', file, upload); + + return path + fileId; + } + }, + { + name: 'rocketchat-avatars-gs', + key(file/*, metaContext*/) { + const path = `${ RocketChat.settings.get('uniqueID') }/avatars/`; + + const user = RocketChat.models.Users.findOneById(this.userId); + + const upload = { + username: user && user.username, + googleCloudStorage: { + bucket: RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'), + path + } + }; + RocketChat.models.Uploads.insertFileInitByUsername(user.username, this.userId, 'googleCloudStorage', file, upload); + + return path + user.username; + } + } + ]; const type = RocketChat.settings.get('FileUpload_Storage_Type'); const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); @@ -79,29 +134,16 @@ const createGoogleStorageDirective = _.debounce(() => { const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); if (type === 'GoogleCloudStorage' && !_.isEmpty(secret) && !_.isEmpty(accessId) && !_.isEmpty(bucket)) { - if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } - - const config = { - bucket, - GoogleAccessId: accessId, - GoogleSecretKey: secret, - key: function _googleCloudStorageKey(file, metaContext) { - const path = `${ RocketChat.settings.get('uniqueID') }/${ metaContext.rid }/${ this.userId }/`; - const fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 'googleCloudStorage', file, { googleCloudStorage: { bucket, path }}); - - return path + fileId; - } - }; - - try { - Slingshot.createDirective(directiveName, Slingshot.GoogleCloud, config); - } catch (e) { - SystemLogger.error('Error configuring GoogleCloudStorage ->', e.message); - } + directives.forEach((conf) => { + console.log('conf.name ->', conf.name); + createDirective(conf.name, { key: conf.key, bucket, accessId, secret }); + }); } else { - delete Slingshot._directives[directiveName]; + directives.forEach((conf) => { + if (Slingshot._directives[conf.name]) { + delete Slingshot._directives[conf.name]; + } + }); } }, 500); diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.coffee b/packages/rocketchat-ui-account/client/avatar/prompt.coffee index 5b4849a1dc..82fa805444 100644 --- a/packages/rocketchat-ui-account/client/avatar/prompt.coffee +++ b/packages/rocketchat-ui-account/client/avatar/prompt.coffee @@ -75,7 +75,7 @@ Template.avatarPrompt.events type: files[0].type # description: document.getElementById('file-description').value - upload = fileUploadHandler 'rocketchat-avatars', record, files[0] + upload = fileUploadHandler 'rocketchat-avatars-gs', record, files[0] # upload.onProgress = (progress) -> # console.log 'progress ->', progress diff --git a/packages/rocketchat-ui/client/lib/fileUpload.js b/packages/rocketchat-ui/client/lib/fileUpload.js index ad91b1fc40..d7ef1590a4 100644 --- a/packages/rocketchat-ui/client/lib/fileUpload.js +++ b/packages/rocketchat-ui/client/lib/fileUpload.js @@ -145,7 +145,7 @@ fileUpload = function(filesToUpload) { description: document.getElementById('file-description').value }; - const upload = fileUploadHandler('rocketchat-uploads', record, file.file); + const upload = fileUploadHandler('rocketchat-uploads-gs', record, file.file); let uploading = Session.get('uploading') || []; uploading.push({ -- GitLab From cc50bf9f08cb8d5232d84f9137ffdac2bdefd5f7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 25 Apr 2017 10:29:42 -0300 Subject: [PATCH 019/280] conversion --- .../rocketchat-ui/client/lib/chatMessages.js | 513 +++++++++++ .../rocketchat-ui/client/lib/cordova/push.js | 89 ++ .../rocketchat-ui/client/lib/cordova/urls.js | 17 + packages/rocketchat-ui/client/lib/modal.js | 111 +++ .../rocketchat-ui/client/lib/msgTyping.js | 72 ++ .../rocketchat-ui/client/lib/notification.js | 141 +++ .../rocketchat-ui/client/lib/readMessages.js | 217 +++++ .../client/lib/recorderjs/audioRecorder.js | 49 + .../client/lib/recorderjs/videoRecorder.js | 98 ++ .../rocketchat-ui/client/views/app/room.js | 839 ++++++++++++++++++ packages/rocketchat-ui/package.js | 16 +- 11 files changed, 2154 insertions(+), 8 deletions(-) create mode 100644 packages/rocketchat-ui/client/lib/chatMessages.js create mode 100644 packages/rocketchat-ui/client/lib/cordova/push.js create mode 100644 packages/rocketchat-ui/client/lib/cordova/urls.js create mode 100644 packages/rocketchat-ui/client/lib/modal.js create mode 100644 packages/rocketchat-ui/client/lib/msgTyping.js create mode 100644 packages/rocketchat-ui/client/lib/notification.js create mode 100644 packages/rocketchat-ui/client/lib/readMessages.js create mode 100644 packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.js create mode 100644 packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js create mode 100644 packages/rocketchat-ui/client/views/app/room.js diff --git a/packages/rocketchat-ui/client/lib/chatMessages.js b/packages/rocketchat-ui/client/lib/chatMessages.js new file mode 100644 index 0000000000..488b3a1a4d --- /dev/null +++ b/packages/rocketchat-ui/client/lib/chatMessages.js @@ -0,0 +1,513 @@ +/* globals MsgTyping */ +import moment from 'moment'; +import toastr from 'toastr'; +this.ChatMessages = class ChatMessages { + init(node) { + this.editing = {}; + this.records = {}; + this.messageMaxSize = RocketChat.settings.get('Message_MaxAllowedSize'); + this.wrapper = $(node).find('.wrapper'); + this.input = $(node).find('.input-message').get(0); + this.$input = $(this.input); + this.hasValue = new ReactiveVar(false); + this.bindEvents(); + } + + resize() { + let dif = (RocketChat.Layout.isEmbedded() ? 0 : 60) + $('.messages-container').find('footer').outerHeight(); + dif += $('.announcement').length > 0 ? 40 : 0; + return $('.messages-box').css({ + height: `calc(100% - ${ dif }px)`}); + } + + getEditingIndex(element) { + const msgs = this.wrapper.get(0).querySelectorAll('.own:not(.system)'); + let index = 0; + for (const msg of Array.from(msgs)) { + if (msg === element) { + return index; + } + index++; + } + return -1; + } + + recordInputAsDraft() { + const { id } = this.editing; + + const message = this.getMessageById(id); + const record = this.records[id] || {}; + const draft = this.input.value; + + if (draft === message.msg) { + return this.clearCurrentDraft(); + } else { + record.draft = draft; + return this.records[id] = record; + } + } + + getMessageDraft(id) { + return this.records[id]; + } + + clearMessageDraft(id) { + return delete this.records[id]; + } + + clearCurrentDraft() { + return this.clearMessageDraft(this.editing.id); + } + + resetToDraft(id) { + const message = this.getMessageById(id); + + const old_value = this.input.value; + this.input.value = message.msg; + + return old_value !== message.msg; + } + + getMessageById(id) { + return ChatMessage.findOne(id); + } + + toPrevMessage() { + const { index } = this.editing; + return this.editByIndex((index != null) ? index - 1 : undefined); + } + + toNextMessage() { + const { index } = this.editing; + if (!this.editByIndex(index + 1)) { return this.clearEditing(); } + } + + editByIndex(index) { + if (!this.editing.element && (index != null)) { return false; } + + const msgs = this.wrapper.get(0).querySelectorAll('.own:not(.system)'); + if ((index == null)) { index = msgs.length - 1; } + + if (!msgs[index]) { return false; } + + const element = msgs[index]; + this.edit(element, index); + + return true; + } + + edit(element, index) { + if ((index == null)) { index = this.getEditingIndex(element); } + + const message = this.getMessageById(element.getAttribute('id')); + + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid); + const editAllowed = RocketChat.settings.get('Message_AllowEditing'); + const editOwn = message && message.u && message.u._id === Meteor.userId(); + + if (!hasPermission && (!editAllowed || !editOwn)) { return; } + if (element.classList.contains('system')) { return; } + + const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); + if (blockEditInMinutes && blockEditInMinutes !== 0) { + let currentTsDiff; + let msgTs; + if (message.ts != null) { msgTs = moment(message.ts); } + if (msgTs != null) { currentTsDiff = moment().diff(msgTs, 'minutes'); } + if (currentTsDiff > blockEditInMinutes) { + return; + } + } + + const draft = this.getMessageDraft(message._id); + let msg = draft && draft.draft; + if (msg == null) { ({ msg } = message); } + + const editingNext = this.editing.index < index; + + // const old_input = this.input.value; + + this.clearEditing(); + + this.hasValue.set(true); + this.editing.element = element; + this.editing.index = index; + this.editing.id = message._id; + element.classList.add('editing'); + this.input.classList.add('editing'); + this.$input.closest('.message-form').addClass('editing'); + + this.input.focus(); + if ((message.attachments != null) && (message.attachments[0].description != null)) { + this.input.value = message.attachments[0].description; + } else { + this.input.value = msg; + } + + const cursor_pos = editingNext ? 0 : -1; + return this.$input.setCursorPosition(cursor_pos); + } + + clearEditing() { + if (this.editing.element) { + this.recordInputAsDraft(); + + this.editing.element.classList.remove('editing'); + this.input.classList.remove('editing'); + this.$input.closest('.message-form').removeClass('editing'); + delete this.editing.id; + delete this.editing.element; + delete this.editing.index; + + this.input.value = this.editing.saved || ''; + const cursor_pos = this.editing.savedCursor != null ? this.editing.savedCursor : -1; + this.$input.setCursorPosition(cursor_pos); + + return this.hasValue.set(this.input.value !== ''); + } else { + this.editing.saved = this.input.value; + return this.editing.savedCursor = this.input.selectionEnd; + } + } + /* globals readMessage KonchatNotification */ + /** + * * @param {string} rim room ID + * * @param {Element} input DOM element + * * @param {function?} done callback + */ + send(rid, input, done) { + if (done == null) { done = function() {}; } + if (_.trim(input.value) !== '') { + readMessage.enable(); + readMessage.readNow(); + $('.message.first-unread').removeClass('first-unread'); + + const msg = input.value; + const msgObject = { _id: Random.id(), rid, msg}; + + // Run to allow local encryption, and maybe other client specific actions to be run before send + return RocketChat.promises.run('onClientBeforeSendMessage', msgObject).then(msgObject => { + + // checks for the final msgObject.msg size before actually sending the message + if (this.isMessageTooLong(msgObject.msg)) { + return toastr.error(t('Message_too_long')); + } + + this.clearCurrentDraft(); + if (this.editing.id) { + this.update(this.editing.id, rid, msgObject.msg); + return; + } + + KonchatNotification.removeRoomNotification(rid); + input.value = ''; + if (typeof input.updateAutogrow === 'function') { + input.updateAutogrow(); + } + this.hasValue.set(false); + this.stopTyping(rid); + + //Check if message starts with /command + if (msg[0] === '/') { + const match = msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m); + if (match != null) { + let command; + if (RocketChat.slashCommands.commands[match[1]]) { + const commandOptions = RocketChat.slashCommands.commands[match[1]]; + command = match[1]; + const param = (match[2] != null) ? match[2] : ''; + if (commandOptions.clientOnly) { + commandOptions.callback(command, param, msgObject); + } else { + Meteor.call('slashCommand', {cmd: command, params: param, msg: msgObject }, (err, result) => typeof commandOptions.result === 'function' ? commandOptions.result(err, result, {cmd: command, params: param, msg: msgObject }) : undefined); + } + return; + } + + if (!RocketChat.settings.get('Message_AllowUnrecognizedSlashCommand')) { + const invalidCommandMsg = { + _id: Random.id(), + rid, + ts: new Date, + msg: TAPi18n.__('No_such_command', { command: match[1] }), + u: { + username: 'rocketbot' + }, + private: true + }; + ChatMessage.upsert({ _id: invalidCommandMsg._id }, invalidCommandMsg); + return; + } + } + } + + Meteor.call('sendMessage', msgObject); + return done(); + }); + + // If edited message was emptied we ask for deletion + } else if (this.editing.element) { + const message = this.getMessageById(this.editing.id); + if (message.attachments && message.attachments[0] && message.attachments[0].description) { + return this.update(this.editing.id, rid, '', true); + } + // Restore original message in textbox in case delete is canceled + this.resetToDraft(this.editing.id); + + return this.confirmDeleteMsg(message, done); + } + } + + confirmDeleteMsg(message, done) { + if (done == null) { done = function() {}; } + if (RocketChat.MessageTypes.isSystemMessage(message)) { return; } + swal({ + title: t('Are_you_sure'), + text: t('You_will_not_be_able_to_recover'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes_delete_it'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: false + }, () => { + swal({ + title: t('Deleted'), + text: t('Your_entry_has_been_deleted'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + + if (this.editing.id === message._id) { + this.clearEditing(message); + } + this.deleteMsg(message); + + this.$input.focus(); + return done(); + }); + + // In order to avoid issue "[Callback not called when still animating](https://github.com/t4t5/sweetalert/issues/528)" + return $('.sweet-alert').addClass('visible'); + } + + deleteMsg(message) { + const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); + if ((blockDeleteInMinutes != null) && (blockDeleteInMinutes !== 0)) { + let msgTs; + if (message.ts != null) { msgTs = moment(message.ts); } + let currentTsDiff; + if (msgTs != null) { currentTsDiff = moment().diff(msgTs, 'minutes'); } + if (currentTsDiff > blockDeleteInMinutes) { + toastr.error(t('Message_deleting_blocked')); + return; + } + } + + return Meteor.call('deleteMessage', { _id: message._id }, function(error) { + if (error) { + return handleError(error); + } + }); + } + + pinMsg(message) { + message.pinned = true; + return Meteor.call('pinMessage', message, function(error) { + if (error) { + return handleError(error); + } + }); + } + + unpinMsg(message) { + message.pinned = false; + return Meteor.call('unpinMessage', message, function(error) { + if (error) { + return handleError(error); + } + }); + } + + update(id, rid, msg, isDescription) { + if ((_.trim(msg) !== '') || (isDescription === true)) { + Meteor.call('updateMessage', { _id: id, msg, rid }); + this.clearEditing(); + return this.stopTyping(rid); + } + } + + startTyping(rid, input) { + if (_.trim(input.value) !== '') { + return MsgTyping.start(rid); + } else { + return MsgTyping.stop(rid); + } + } + + stopTyping(rid) { + return MsgTyping.stop(rid); + } + + bindEvents() { + if (this.wrapper != null ? this.wrapper.length : undefined) { + return $('.input-message').autogrow({ + postGrowCallback: () => { + return this.resize(); + } + }); + } + } + + tryCompletion(input) { + const [value] = input.value.match(/[^\s]+$/) || []; + if (!value) { return; } + const re = new RegExp(value, 'i'); + const user = Meteor.users.findOne({username: re}); + if (user != null) { + return input.value = input.value.replace(value, `@${ user.username } `); + } + } + + keyup(rid, event) { + let i; + const input = event.currentTarget; + const k = event.which; + const keyCodes = [ + 13, // Enter + 20, // Caps lock + 16, // Shift + 9, // Tab + 27, // Escape Key + 17, // Control Key + 91, // Windows Command Key + 19, // Pause Break + 18, // Alt Key + 93, // Right Click Point Key + 45, // Insert Key + 34, // Page Down + 35, // Page Up + 144, // Num Lock + 145 // Scroll Lock + ]; + for (i = 35; i <= 40; i++) { keyCodes.push(i); } // Home, End, Arrow Keys + for (i = 112; i <= 123; i++) { keyCodes.push(i); } // F1 - F12 + + if (!Array.from(keyCodes).includes(k)) { + this.startTyping(rid, input); + } + + return this.hasValue.set(input.value !== ''); + } + + keydown(rid, event) { + const user = Meteor.user(); + const sendOnEnter = user && user.settings && user.settings.preferences && user.settings.preferences.sendOnEnter; + const input = event.currentTarget; + // const $input = $(input); + const k = event.which; + this.resize(input); + + if (k === 13) { + if (sendOnEnter == null || sendOnEnter === 'normal' || (sendOnEnter === 'desktop' && Meteor.Device.isDesktop())) { + if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { // Enter without shift/ctrl/alt + event.preventDefault(); + event.stopPropagation(); + this.send(rid, input); + return; + } else if (!event.shiftKey) { + return input.value +='\n'; + } + } else if (sendOnEnter === 'alternative') { + if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) { // Enter with shift/ctrl/alt + event.preventDefault(); + event.stopPropagation(); + this.send(rid, input); + return; + } + } + } + + + + if (k === 9) { // Tab + event.preventDefault(); + event.stopPropagation(); + this.tryCompletion(input); + } + + if (k === 27) { // Escape + if (this.editing.index != null) { + // const record = this.getMessageDraft(this.editing.id); + + // If resetting did nothing then edited message is same as original + if (!this.resetToDraft(this.editing.id)) { + this.clearCurrentDraft(); + this.clearEditing(); + } + + event.preventDefault(); + event.stopPropagation(); + return; + } + } else if ((k === 38) || (k === 40)) { // Arrow Up or down + if (event.shiftKey) { return true; } + + const cursor_pos = input.selectionEnd; + + if (k === 38) { // Arrow Up + if (cursor_pos === 0) { + this.toPrevMessage(); + } else if (!event.altKey) { + return true; + } + + if (event.altKey) { this.$input.setCursorPosition(0); } + + } else { // Arrow Down + if (cursor_pos === input.value.length) { + this.toNextMessage(); + } else if (!event.altKey) { + return true; + } + + if (event.altKey) { this.$input.setCursorPosition(-1); } + } + + return false; + + // ctrl (command) + shift + k -> clear room messages + } else if (k === 75 && navigator && navigator.platform && event.shiftKey && (navigator.platform.indexOf('Mac') !== -1 && event.metaKey) || event.ctrlKey) { + return RoomHistoryManager.clear(rid); + } + } + + valueChanged(/*rid, event*/) { + if (this.input.value.length === 1) { + return this.determineInputDirection(); + } + } + + determineInputDirection() { + return this.input.dir = this.isMessageRtl(this.input.value) ? 'rtl' : 'ltr'; + } + + // http://stackoverflow.com/a/14824756 + isMessageRtl(message) { + const ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF'; + const rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; + const rtlDirCheck = new RegExp(`^[^${ ltrChars }]*[${ rtlChars }]`); + + return rtlDirCheck.test(message); + } + + isMessageTooLong(message) { + return (message != null ? message.length : undefined) > this.messageMaxSize; + } + + isEmpty() { + return !this.hasValue.get(); + } +}; diff --git a/packages/rocketchat-ui/client/lib/cordova/push.js b/packages/rocketchat-ui/client/lib/cordova/push.js new file mode 100644 index 0000000000..7d322604d3 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/cordova/push.js @@ -0,0 +1,89 @@ +/* globals Push Servers*/ +if (Meteor.isCordova) { + // Push.addListener 'token', (token) -> + // Meteor.call 'log', 'CLIENT', 'token', arguments + + // Push.addListener 'error', (err) -> + // Meteor.call 'log', 'CLIENT', 'error', arguments + // if err.type == 'apn.cordova' + // Meteor.call 'log', 'CLIENT', err.error + + // Push.addListener 'register', (evt) -> + // Meteor.call 'log', 'CLIENT', 'register', arguments + + // Push.addListener 'alert', (notification) -> + // Meteor.call 'log', 'CLIENT', 'alert', arguments + + // Push.addListener 'sound', (notification) -> + // Meteor.call 'log', 'CLIENT', 'sound', arguments + + // Push.addListener 'badge', (notification) -> + // Meteor.call 'log', 'CLIENT', 'badge', arguments + + // Push.addListener 'message', (notification) -> + // Meteor.call 'log', 'CLIENT', 'message', arguments + + Push.addListener('startup', function(notification) { + // Meteor.call 'log', 'CLIENT', 'startup', arguments + + if ((notification.payload != null ? notification.payload.rid : undefined) != null) { + if (notification.payload.host === Meteor.absoluteUrl()) { + switch (notification.payload.type) { + case 'c': + return FlowRouter.go('channel', { name: notification.payload.name }, FlowRouter.current().queryParams); + case 'p': + return FlowRouter.go('group', { name: notification.payload.name }, FlowRouter.current().queryParams); + case 'd': + return FlowRouter.go('direct', { username: notification.payload.sender.username }, FlowRouter.current().queryParams); + } + } else { + let path = ''; + switch (notification.payload.type) { + case 'c': + path = `channel/${ notification.payload.name }`; + break; + case 'p': + path = `group/${ notification.payload.name }`; + break; + case 'd': + path = `direct/${ notification.payload.sender.username }`; + break; + } + + const host = notification.payload.host.replace(/\/$/, ''); + if (Servers.serverExists(host) !== true) { + return; + } + + return Servers.startServer(host, path, function(err) { + if (err != null) { + // TODO err + return console.log(err); + } + }); + } + } + }); + + + Meteor.startup(() => + Tracker.autorun(function() { + if (RocketChat.settings.get('Push_enable') === true) { + + Push.Configure({ + android: { + senderID: window.ANDROID_SENDER_ID, + sound: true, + vibrate: true + }, + ios: { + badge: true, + clearBadge: true, + sound: true, + alert: true + } + }); + } + }) + ); +} diff --git a/packages/rocketchat-ui/client/lib/cordova/urls.js b/packages/rocketchat-ui/client/lib/cordova/urls.js new file mode 100644 index 0000000000..ffac42ed71 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/cordova/urls.js @@ -0,0 +1,17 @@ +Meteor.startup(function() { + if (!Meteor.isCordova) { return; } + // Handle click events for all external URLs + $(document).on('deviceready', function() { + // const platform = device.platform.toLowerCase(); + $(document).on('click', function(e) { + const $link = $(e.target).closest('a[href]'); + if (!($link.length > 0)) { return; } + const url = $link.attr('href'); + + if (/^https?:\/\/.+/.test(url) === true) { + window.open(url, '_system'); + return e.preventDefault(); + } + }); + }); +}); diff --git a/packages/rocketchat-ui/client/lib/modal.js b/packages/rocketchat-ui/client/lib/modal.js new file mode 100644 index 0000000000..226b1aa5fd --- /dev/null +++ b/packages/rocketchat-ui/client/lib/modal.js @@ -0,0 +1,111 @@ +this.Modal = (function() { + + const self = {}; + const win = $(window); + + //mistérios da vida c.483: Pq a self.$window diz ter 100% da janela via css mas na verdade ocupa menos de 100% da tela? + //isso impede que o retorno da janela ao normal quando não mais necessária a classe fluid. (comportamento dançante) + + function focus() { + if (self.$modal) { + const input = self.$modal.find('input[type=\'text\']'); + if (input.length) { return input.get(0).focus(); } + } + } + function check() { + if (self.$modal && self.$modal.length) { + if (win.height() < (self.$window.outerHeight() + (win.height() * 0.10))) { + if (!self.$modal.hasClass('fluid')) { + return self.$modal.addClass('fluid'); + } + } + } + } + function stopListening() { + if (self.interval) { return clearInterval(self.interval); } + } + function startListening() { + stopListening(); + return self.interval = setInterval(() => check() + , 100); + } + + + function close() { + self.$modal.addClass('closed'); + win.unbind('keydown.modal'); + // acionar no on-complete da animação + return setTimeout(function() { + self.opened = 0; + stopListening(); + return self.$modal.removeClass('opened closed'); + } + , 300); + } + function keydown(e) { + const k = e.which; + if (k === 27) { + e.preventDefault(); + e.stopImmediatePropagation(); + return close(); + } + } + function checkFooter() { + if (self.$footer && self.$footer.length) { + const buttons = self.$footer.find('button'); + return buttons.each(function() { + const btn = $(this); + if (btn.html().match(/fechar/ig)) { + return btn.click(function(e) { + e.preventDefault(); + return close(); + }); + } + }); + } + } + + function setContent(template, data) { + self.$main.empty(); + if (template) { + if (data) { + Blaze.renderWithData(template, data, self.$main.get(0)); + } else { + Blaze.render(template, self.$main.get(0)); + } + checkFooter(); + return check(); + } + } + + function open(template, params) { + params = params || {}; + RocketChat.animeBack(self.$modal, () => focus()); + self.opened = 1; + if (params.listening) { startListening(); } + if (template != null) { setContent(template, params.data); } + self.$modal.addClass('opened'); + self.$modal.removeClass('fluid'); + return setTimeout(() => focus() + , 200); + } + + function init($modal, params) { + self.params = params || {}; + self.opened = 0; + self.initialized = 0; + self.$modal = $modal.length ? $modal : $('.rocket-modal'); + if (self.$modal.length) { + self.initialized = 0; + self.$window = self.$modal.find('.modal'); + self.$main = self.$modal.find('main'); + self.$close = self.$modal.find('header > .close'); + self.$footer = self.$modal.find('footer'); + self.$close.unbind('click').click(close); + win.unbind('resize.modal').bind('resize.modal', check); + return win.unbind('keydown.modal').bind('keydown.modal', e => keydown(e)); + } + } + + return { init, open, close, focus, setContent }; +}()); diff --git a/packages/rocketchat-ui/client/lib/msgTyping.js b/packages/rocketchat-ui/client/lib/msgTyping.js new file mode 100644 index 0000000000..73b388d93f --- /dev/null +++ b/packages/rocketchat-ui/client/lib/msgTyping.js @@ -0,0 +1,72 @@ +export const MsgTyping = (function() { + const timeout = 15000; + const timeouts = {}; + let renew = true; + const renewTimeout = 10000; + const selfTyping = new ReactiveVar(false); + const usersTyping = {}; + const dep = new Tracker.Dependency; + + const addStream = function(room) { + if (!_.isEmpty(usersTyping[room] && usersTyping[room].users)) { + return; + } + usersTyping[room] = { users: {} }; + return RocketChat.Notifications.onRoom(room, 'typing', function(username, typing) { + const user = Meteor.user(); + if (username === (user && user.username)) { + return; + } + const { users } = usersTyping[room]; + if (typing === true) { + users[username] = Meteor.setTimeout(function() { + delete users[username]; + usersTyping[room].users = users; + return dep.changed(); + }, timeout); + } else { + delete users[username]; + } + usersTyping[room].users = users; + return dep.changed(); + }); + }; + + Tracker.autorun(() => Session.get('openedRoom') && addStream(Session.get('openedRoom'))); + const stop = function(room) { + renew = true; + selfTyping.set(false); + if ((timeouts != null ? timeouts[room] : undefined) != null) { + clearTimeout(timeouts[room]); + timeouts[room] = null; + } + const user = Meteor.user(); + return RocketChat.Notifications.notifyRoom(room, 'typing', user && user.username, false); + }; + const start = function(room) { + if (!renew) { return; } + + setTimeout(() => renew = true, renewTimeout); + + renew = false; + selfTyping.set(true); + const user = Meteor.user(); + RocketChat.Notifications.notifyRoom(room, 'typing', user && user.username, true); + clearTimeout(timeouts[room]); + return timeouts[room] = Meteor.setTimeout(() => stop(room), timeout); + }; + + + const get = function(room) { + dep.depend(); + if (!usersTyping[room]) { + usersTyping[room] = { users: {} }; + } + const { users } = usersTyping[room]; + return _.keys(users) || []; + }; + + return { start, stop, get, selfTyping }; +}()); + +this.MsgTyping = MsgTyping; diff --git a/packages/rocketchat-ui/client/lib/notification.js b/packages/rocketchat-ui/client/lib/notification.js new file mode 100644 index 0000000000..94c378ab32 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/notification.js @@ -0,0 +1,141 @@ +// @TODO implementar 'clicar na notificacao' abre a janela do chat +const KonchatNotification = { + notificationStatus: new ReactiveVar, + + // notificacoes HTML5 + getDesktopPermission() { + if (window.Notification && (Notification.permission !== 'granted') && !Meteor.settings.public.sandstorm) { + return Notification.requestPermission(function(status) { + KonchatNotification.notificationStatus.set(status); + if (Notification.permission !== status) { + return Notification.permission = status; + } + }); + } + }, + + notify(notification) { + if (window.Notification && Notification.permission === 'granted') { + const message = { rid: (notification.payload != null ? notification.payload.rid : undefined), msg: notification.text, notification: true }; + return RocketChat.promises.run('onClientMessageReceived', message).then(function(message) { + const n = new Notification(notification.title, { + icon: notification.icon || getAvatarUrlFromUsername(notification.payload.sender.username), + body: _.stripTags(message.msg), + tag: notification.payload._id, + silent: true, + canReply: true + }); + + const user = Meteor.user(); + + const notificationDuration = notification.duration - 0 || user && user.settings && user.settings.preferences && user.settings.preferences.desktopNotificationDuration - 0 || RocketChat.settings.get('Desktop_Notifications_Duration'); + if (notificationDuration > 0) { + setTimeout((() => n.close()), notificationDuration * 1000); + } + + if (notification.payload && notification.payload.rid) { + if (n.addEventListener) { + n.addEventListener('reply', ({response}) => + Meteor.call('sendMessage', { + _id: Random.id(), + rid: notification.payload.rid, + msg: response + }) + ); + } + + n.onclick = function() { + this.close(); + window.focus(); + switch (notification.payload.type) { + case 'd': + return FlowRouter.go('direct', { username: notification.payload.sender.username }, FlowRouter.current().queryParams); + case 'c': + return FlowRouter.go('channel', { name: notification.payload.name }, FlowRouter.current().queryParams); + case 'p': + return FlowRouter.go('group', { name: notification.payload.name }, FlowRouter.current().queryParams); + } + }; + } + }); + } + }, + + showDesktop(notification) { + if ((notification.payload.rid === Session.get('openedRoom')) && (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined)) { + return; + } + + if ((Meteor.user().status === 'busy') || (Meteor.settings.public.sandstorm != null)) { + return; + } + /* globals getAvatarAsPng*/ + return getAvatarAsPng(notification.payload.sender.username, function(avatarAsPng) { + notification.icon = avatarAsPng; + return KonchatNotification.notify(notification); + }); + }, + + newMessage(rid) { + if (!Session.equals(`user_${ Meteor.userId() }_status`, 'busy')) { + const user = Meteor.user(); + const newMessageNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newMessageNotification || 'chime'; + const sub = ChatSubscription.findOne({ rid }, { fields: { audioNotification: 1 } }); + if ((sub && sub.audioNotification) !== 'none') { + if (sub && sub.audioNotification) { + const [audio] = $(`audio#${ sub.audioNotification }`); + return audio && audio.play && audio.play(); + } else if (newMessageNotification !== 'none') { + const [audio] = $(`audio#${ newMessageNotification }`); + return audio && audio.play && audio.play(); + } + } + } + }, + + newRoom(rid, withSound) { + if (withSound == null) { withSound = true; } + Tracker.nonreactive(function() { + let newRoomSound = Session.get('newRoomSound'); + if (newRoomSound != null) { + newRoomSound = _.union(newRoomSound, rid); + } else { + newRoomSound = [rid]; + } + + return Session.set('newRoomSound', newRoomSound); + }); + }, + + // $('.link-room-' + rid).addClass('new-room-highlight') + + removeRoomNotification(rid) { + Tracker.nonreactive(() => Session.set('newRoomSound', [])); + + return $(`.link-room-${ rid }`).removeClass('new-room-highlight'); + } +}; + +Tracker.autorun(function() { + const user = Meteor.user(); + const newRoomNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newRoomNotification || 'door'; + if ((Session.get('newRoomSound') || []) > 0) { + Tracker.nonreactive(function() { + if (!Session.equals(`user_${ Meteor.userId() }_status`, 'busy') && newRoomNotification !== 'none') { + const [audio] = $(`audio#${ newRoomNotification }`); + return audio && audio.play && audio.play(); + } + }); + } else { + const [room] = $(`audio#${ newRoomNotification }`); + if (!room) { + return; + } + if (room.pause) { + room.pause(); + return room.currentTime = 0; + } + } +}); +export { KonchatNotification }; +this.KonchatNotification = KonchatNotification; diff --git a/packages/rocketchat-ui/client/lib/readMessages.js b/packages/rocketchat-ui/client/lib/readMessages.js new file mode 100644 index 0000000000..c5afd8cc08 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/readMessages.js @@ -0,0 +1,217 @@ +/* DEFINITIONS +- If window loses focus user needs to scroll or click/touch some place +- On hit ESC enable read, force read of current room and remove unread mark +- When user change room disable read until user interaction +- Only read if mark of *first-unread* is visible for user or if flag *force* was passed +- Always read the opened room +- The default method *read* has a delay of 2000ms to prevent multiple reads and to user be able to see the mark +*/ + +// Meteor.startup -> + // window.addEventListener 'focus', -> + // readMessage.refreshUnreadMark(undefined, true) + +const readMessage = new class { + constructor() { + this.debug = false; + this.callbacks = []; + this.read = _.debounce((force) => this.readNow(force), 1000); + this.canReadMessage = false; + } + + readNow(force) { + if (force == null) { force = false; } + if (this.debug) { console.log('--------------'); } + if (this.debug) { console.log('readMessage -> readNow init process force:', force); } + + const self = this; + + self.refreshUnreadMark(); + + if ((force !== true) && (this.canReadMessage === false)) { + if (this.debug) { console.log('readMessage -> readNow canceled by canReadMessage: false'); } + return; + } + + const rid = Session.get('openedRoom'); + if (rid == null) { + if (this.debug) { console.log('readMessage -> readNow canceled, no rid informed'); } + return; + } + + if (force === true) { + if (this.debug) { console.log('readMessage -> readNow via force rid:', rid); } + return Meteor.call('readMessages', rid, function() { + RoomHistoryManager.getRoom(rid).unreadNotLoaded.set(0); + self.refreshUnreadMark(); + return self.fireRead(rid); + }); + } + + const subscription = ChatSubscription.findOne({rid}); + if (subscription == null) { + if (this.debug) { console.log('readMessage -> readNow canceled, no subscription found for rid:', rid); } + return; + } + + if ((subscription.alert === false) && (subscription.unread === 0)) { + if (this.debug) { console.log('readMessage -> readNow canceled, alert', subscription.alert, 'and unread', subscription.unread); } + return; + } + + const room = RoomManager.getOpenedRoomByRid(rid); + if (room == null) { + if (this.debug) { console.log('readMessage -> readNow canceled, no room found for typeName:', subscription.t + subscription.name); } + return; + } + + // Only read messages if user saw the first unread message + const unreadMark = $('.message.first-unread'); + if (unreadMark.length > 0) { + const position = unreadMark.position(); + const visible = (position != null ? position.top : undefined) >= 0; + if (!visible && (room.unreadSince.get() != null)) { + if (this.debug) { console.log('readMessage -> readNow canceled, unread mark visible:', visible, 'unread since exists', (room.unreadSince.get() != null)); } + return; + } + } + + if (this.debug) { console.log('readMessage -> readNow rid:', rid); } + Meteor.call('readMessages', rid, function() { + RoomHistoryManager.getRoom(rid).unreadNotLoaded.set(0); + self.refreshUnreadMark(); + return self.fireRead(rid); + }); + } + + disable() { + return this.canReadMessage = false; + } + + enable() { + return this.canReadMessage = document.hasFocus(); + } + + isEnable() { + return this.canReadMessage === true; + } + + onRead(cb) { + return this.callbacks.push(cb); + } + + fireRead(rid) { + return Array.from(this.callbacks).map((cb) => cb(rid)); + } + + refreshUnreadMark(rid, force) { + if (rid == null) { rid = Session.get('openedRoom'); } + if (rid == null) { + return; + } + + const subscription = ChatSubscription.findOne({rid}, {reactive: false}); + if (subscription == null) { + return; + } + + const room = RoomManager.openedRooms[subscription.t + subscription.name]; + if (room == null) { + return; + } + + const $roomDom = $(room.dom); + $roomDom.find('.message.first-unread').addClass('first-unread-opaque'); + + if (!subscription.alert && (subscription.unread === 0)) { + room.unreadSince.set(undefined); + return; + } + + if ((force == null) && (subscription.rid === Session.get('openedRoom')) && document.hasFocus()) { + return; + } + + $roomDom.find('.message.first-unread').removeClass('first-unread').removeClass('first-unread-opaque'); + + let lastReadRecord = ChatMessage.findOne({ + rid: subscription.rid, + ts: { + $lt: subscription.ls + } + } + // 'u._id': + // $ne: Meteor.userId() + , { + sort: { + ts: -1 + } + } + ); + + if ((lastReadRecord == null) && (RoomHistoryManager.getRoom(room.rid).unreadNotLoaded.get() === 0)) { + lastReadRecord = + {ts: new Date(0)}; + } + + if ((lastReadRecord != null) || (RoomHistoryManager.getRoom(room.rid).unreadNotLoaded.get() > 0)) { + room.unreadSince.set(subscription.ls); + } else { + room.unreadSince.set(undefined); + } + + if (lastReadRecord != null) { + const firstUnreadRecord = ChatMessage.findOne({ + rid: subscription.rid, + ts: { + $gt: lastReadRecord.ts + }, + 'u._id': { + $ne: Meteor.userId() + } + } + , { + sort: { + ts: 1 + } + } + ); + + if (firstUnreadRecord != null) { + room.unreadFirstId = firstUnreadRecord._id; + return $roomDom.find(`.message#${ firstUnreadRecord._id }`).addClass('first-unread'); + } + } + } +}; + + +Meteor.startup(function() { + $(window).on('blur', () => readMessage.disable()); + + $(window).on('focus', () => { + readMessage.enable(); + return readMessage.read(); + }); + + $(window).on('click', () => { + readMessage.enable(); + return readMessage.read(); + }); + + $(window).on('touchend', () => { + readMessage.enable(); + return readMessage.read(); + }); + + $(window).on('keyup', (e) => { + const key = e.which; + if (key === 27) { + readMessage.enable(); + readMessage.readNow(true); + return $('.message.first-unread').removeClass('first-unread'); + } + }); +}); +export { readMessage }; +this.readMessage = readMessage; diff --git a/packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.js b/packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.js new file mode 100644 index 0000000000..be28afa739 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.js @@ -0,0 +1,49 @@ +/* globals Recorder */ +this.AudioRecorder = new class { + start(cb) { + window.AudioContext = window.AudioContext || window.webkitAudioContext; + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia; + window.URL = window.URL || window.webkitURL; + + window.audioContext = new AudioContext; + + const ok = stream => { + this.startUserMedia(stream); + return (cb != null ? cb.call(this) : undefined); + }; + + if ((navigator.getUserMedia == null)) { + return cb(false); + } + + return navigator.getUserMedia({audio: true}, ok, e => console.log(`No live audio input: ${ e }`)); + } + + startUserMedia(stream) { + this.stream = stream; + const input = window.audioContext.createMediaStreamSource(stream); + this.recorder = new Recorder(input, {workerPath: '/recorderWorker.js'}); + return this.recorder.record(); + } + + stop(cb) { + this.recorder.stop(); + + if (cb != null) { + this.getBlob(cb); + } + + this.stream.getAudioTracks()[0].stop(); + + this.recorder.clear(); + + window.audioContext.close(); + delete window.audioContext; + delete this.recorder; + return delete this.stream; + } + + getBlob(cb) { + return this.recorder.exportWAV(cb); + } +}; diff --git a/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js b/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js new file mode 100644 index 0000000000..8ae2be4851 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js @@ -0,0 +1,98 @@ +this.VideoRecorder = new class { + constructor() { + this.started = false; + this.cameraStarted = new ReactiveVar(false); + this.recording = new ReactiveVar(false); + this.recordingAvailable = new ReactiveVar(false); + } + + start(videoel, cb) { + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + window.URL = window.URL || window.webkitURL; + + this.videoel = videoel; + const ok = stream => { + this.startUserMedia(stream); + return (cb != null ? cb.call(this) : undefined); + }; + + if ((navigator.getUserMedia == null)) { + return cb(false); + } + + return navigator.getUserMedia({audio: true, video: true}, ok, e => console.log(`No live video input: ${ e }`)); + } + + record() { + this.chunks = []; + if ((this.stream == null)) { + return; + } + this.mediaRecorder = new MediaRecorder(this.stream); + this.mediaRecorder.stream = this.stream; + this.mediaRecorder.mimeType = 'video/webm'; + this.mediaRecorder.ondataavailable = blobev => { + this.chunks.push(blobev.data); + if (!this.recordingAvailable.get()) { + return this.recordingAvailable.set(true); + } + }; + this.mediaRecorder.start(); + return this.recording.set(true); + } + + startUserMedia(stream) { + this.stream = stream; + this.videoel.src = URL.createObjectURL(stream); + this.videoel.onloadedmetadata = () => { + return this.videoel.play(); + }; + + this.started = true; + return this.cameraStarted.set(true); + } + + stop(cb) { + if (this.started) { + this.stopRecording(); + + if (this.stream != null) { + const vtracks = this.stream.getVideoTracks(); + for (const vtrack of Array.from(vtracks)) { + vtrack.stop(); + } + + const atracks = this.stream.getAudioTracks(); + for (const atrack of Array.from(atracks)) { + atrack.stop(); + } + } + + if (this.videoel != null) { + this.videoel.pause; + this.videoel.src = ''; + } + + this.started = false; + this.cameraStarted.set(false); + this.recordingAvailable.set(false); + + if ((cb != null) && (this.chunks != null)) { + const blob = new Blob(this.chunks, { 'type' : 'video/webm' }); + cb(blob); + } + + delete this.recorder; + delete this.stream; + return delete this.videoel; + } + } + + stopRecording() { + if (this.started && this.recording && this.mediaRecorder != null) { + this.mediaRecorder.stop(); + this.recording.set(false); + return delete this.mediaRecorder; + } + } +}; diff --git a/packages/rocketchat-ui/client/views/app/room.js b/packages/rocketchat-ui/client/views/app/room.js new file mode 100644 index 0000000000..f6e21d9015 --- /dev/null +++ b/packages/rocketchat-ui/client/views/app/room.js @@ -0,0 +1,839 @@ +/* globals RocketChatTabBar , fileUpload , fireGlobalEvent , mobileMessageMenu , cordova , readMessage , RoomRoles*/ +import moment from 'moment'; +import mime from 'mime-type/with-db'; + + +const socialSharing = (options = {}) => window.plugins.socialsharing.share(options.message, options.subject, options.file, options.link); + +const isSubscribed = _id => ChatSubscription.find({ rid: _id }).count() > 0; + +const favoritesEnabled = () => RocketChat.settings.get('Favorite_Rooms'); + +const userCanDrop = _id => !RocketChat.roomTypes.readOnly(_id, Meteor.user()); + +Template.room.helpers({ + isTranslated() { + const sub = ChatSubscription.findOne({ rid: this._id }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } }); + return RocketChat.settings.get('AutoTranslate_Enabled') && ((sub != null ? sub.autoTranslate : undefined) === true) && (sub.autoTranslateLanguage != null); + }, + + embeddedVersion() { + return RocketChat.Layout.isEmbedded(); + }, + + favorite() { + const sub = ChatSubscription.findOne({ rid: this._id }, { fields: { f: 1 } }); + if (((sub != null ? sub.f : undefined) != null) && sub.f && favoritesEnabled()) { return 'icon-star favorite-room pending-color'; } + return 'icon-star-empty'; + }, + + favoriteLabel() { + const sub = ChatSubscription.findOne({ rid: this._id }, { fields: { f: 1 } }); + if (((sub != null ? sub.f : undefined) != null) && sub.f && favoritesEnabled()) { return 'Unfavorite'; } + return 'Favorite'; + }, + + subscribed() { + return isSubscribed(this._id); + }, + + messagesHistory() { + const hideMessagesOfType = []; + RocketChat.settings.collection.find({_id: /Message_HideType_.+/}).forEach(function(record) { + let types; + const type = record._id.replace('Message_HideType_', ''); + switch (type) { + case 'mute_unmute': + types = [ 'user-muted', 'user-unmuted' ]; + break; + default: + types = [ type ]; + } + return types.forEach(function(type) { + const index = hideMessagesOfType.indexOf(type); + + if ((record.value === true) && (index === -1)) { + return hideMessagesOfType.push(type); + } else if (index > -1) { + return hideMessagesOfType.splice(index, 1); + } + }); + }); + + const query = + {rid: this._id}; + + if (hideMessagesOfType.length > 0) { + query.t = + {$nin: hideMessagesOfType}; + } + + const options = { + sort: { + ts: 1 + } + }; + + return ChatMessage.find(query, options); + }, + + hasMore() { + return RoomHistoryManager.hasMore(this._id); + }, + + hasMoreNext() { + return RoomHistoryManager.hasMoreNext(this._id); + }, + + isLoading() { + return RoomHistoryManager.isLoading(this._id); + }, + + windowId() { + return `chat-window-${ this._id }`; + }, + + uploading() { + return Session.get('uploading'); + }, + + roomName() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { return ''; } + + return RocketChat.roomTypes.getRoomName(roomData.t, roomData); + }, + + secondaryName() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { return ''; } + + return RocketChat.roomTypes.getSecondaryRoomName(roomData.t, roomData); + }, + + roomTopic() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { return ''; } + return roomData.topic; + }, + + showAnnouncement() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { return false; } + Meteor.defer(() => { + if (window.chatMessages && window.chatMessages[roomData._id]) { + return window.chatMessages[roomData._id].resize(); + } + }); + return (roomData.announcement !== undefined) && (roomData.announcement !== ''); + }, + + roomAnnouncement() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { return ''; } + return roomData.announcement; + }, + + roomIcon() { + const roomData = Session.get(`roomData${ this._id }`); + if (!(roomData != null ? roomData.t : undefined)) { return ''; } + + return RocketChat.roomTypes.getIcon(roomData != null ? roomData.t : undefined); + }, + + userStatus() { + const roomData = Session.get(`roomData${ this._id }`); + return RocketChat.roomTypes.getUserStatus(roomData.t, this._id) || 'offline'; + }, + + maxMessageLength() { + return RocketChat.settings.get('Message_MaxAllowedSize'); + }, + + unreadData() { + const data = + {count: RoomHistoryManager.getRoom(this._id).unreadNotLoaded.get() + Template.instance().unreadCount.get()}; + + const room = RoomManager.getOpenedRoomByRid(this._id); + if (room != null) { + data.since = room.unreadSince != null ? room.unreadSince.get() : undefined; + } + + return data; + }, + + containerBarsShow(unreadData, uploading) { + if ((((unreadData != null ? unreadData.count : undefined) > 0) && (unreadData.since != null)) || ((uploading != null ? uploading.length : undefined) > 0)) { return 'show'; } + }, + + formatUnreadSince() { + if ((this.since == null)) { return; } + + return moment(this.since).calendar(null, {sameDay: 'LT'}); + }, + + flexData() { + const flexData = { + tabBar: Template.instance().tabBar, + data: { + rid: this._id, + userDetail: Template.instance().userDetail.get(), + clearUserDetail: Template.instance().clearUserDetail + } + }; + + return flexData; + }, + + adminClass() { + if (RocketChat.authz.hasRole(Meteor.userId(), 'admin')) { return 'admin'; } + }, + + showToggleFavorite() { + if (isSubscribed(this._id) && favoritesEnabled()) { return true; } + }, + + viewMode() { + const user = Meteor.user(); + const viewMode = user && user.settings && user.settings.preferences && user.settings.preferences.viewMode; + const modes = ['', 'cozy', 'compact']; + return modes[viewMode] || modes[0]; + }, + + selectable() { + return Template.instance().selectable.get(); + }, + + hideUsername() { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.hideUsernames ? 'hide-usernames' : undefined; + }, + + hideAvatar() { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.hideAvatars ? 'hide-avatars' : undefined; + }, + + userCanDrop() { + return userCanDrop(this._id); + }, + + canPreview() { + const room = Session.get(`roomData${ this._id }`); + if (room.t !== 'c') { + return true; + } + + if (RocketChat.authz.hasAllPermission('preview-c-room')) { + return true; + } + + return (RocketChat.models.Subscriptions.findOne({rid: this._id}) != null); + } +}); + +let isSocialSharingOpen = false; +let touchMoved = false; + +Template.room.events({ + 'click, touchend'(e, t) { + return Meteor.setTimeout(() => t.sendToBottomIfNecessaryDebounced() + , 100); + }, + + 'click .messages-container'() { + const user = Meteor.user(); + + if ((Template.instance().tabBar.getState() === 'opened') && user && user.settings && user.settings.preferences && user.settings.preferences.hideFlexTab) { + return Template.instance().tabBar.close(); + } + }, + + 'touchstart .message'(e, t) { + touchMoved = false; + isSocialSharingOpen = false; + if (e.originalEvent.touches.length !== 1) { + return; + } + + if ($(e.currentTarget).hasClass('system')) { + return; + } + + if (e.target && (e.target.nodeName === 'AUDIO')) { + return; + } + + if (e.target && (e.target.nodeName === 'A') && /^https?:\/\/.+/.test(e.target.getAttribute('href'))) { + e.preventDefault(); + e.stopPropagation(); + } + + const message = this._arguments[1]; + const doLongTouch = () => { + + if ((window.plugins != null ? window.plugins.socialsharing : undefined) != null) { + isSocialSharingOpen = true; + + if (e.target && (e.target.nodeName === 'A') && /^https?:\/\/.+/.test(e.target.getAttribute('href'))) { + if (message.attachments != null) { + const attachment = _.find(message.attachments, item => item.title === e.target.innerText); + if (attachment != null) { + socialSharing({ + file: e.target.href, + subject: e.target.innerText, + message: message.msg + }); + return; + } + } + + socialSharing({ + link: e.target.href, + subject: e.target.innerText, + message: message.msg + }); + return; + } + + if (e.target && (e.target.nodeName === 'IMG')) { + socialSharing({ + file: e.target.src, + message: message.msg + }); + return; + } + } + + return mobileMessageMenu.show(message, t, e, this); + }; + + Meteor.clearTimeout(t.touchtime); + return t.touchtime = Meteor.setTimeout(doLongTouch, 500); + }, + + 'click .message img'(e, t) { + Meteor.clearTimeout(t.touchtime); + if ((isSocialSharingOpen === true) || (touchMoved === true)) { + e.preventDefault(); + return e.stopPropagation(); + } + }, + + 'touchend .message'(e, t) { + Meteor.clearTimeout(t.touchtime); + if (isSocialSharingOpen === true) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.target && (e.target.nodeName === 'A') && /^https?:\/\/.+/.test(e.target.getAttribute('href'))) { + if (touchMoved === true) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + if ((typeof cordova !== 'undefined' && cordova !== null ? cordova.InAppBrowser : undefined) != null) { + return cordova.InAppBrowser.open(e.target.href, '_system'); + } else { + return window.open(e.target.href); + } + } + }, + + 'touchmove .message'(e, t) { + touchMoved = true; + return Meteor.clearTimeout(t.touchtime); + }, + + 'touchcancel .message'(e, t) { + return Meteor.clearTimeout(t.touchtime); + }, + + 'click .upload-progress-text > button'(e) { + e.preventDefault(); + return Session.set(`uploading-cancel-${ this.id }`, true); + }, + + 'click .unread-bar > button.mark-read'() { + return readMessage.readNow(true); + }, + + 'click .unread-bar > button.jump-to'(e, t) { + const { _id } = t.data; + const room = RoomHistoryManager.getRoom(_id); + let message = room && readMessage.firstUnread.get(); + if (message) { + return RoomHistoryManager.getSurroundingMessages(message, 50); + } else { + const subscription = ChatSubscription.findOne({ rid: _id }); + message = ChatMessage.find({ rid: _id, ts: { $gt: (subscription != null ? subscription.ls : undefined) } }, { sort: { ts: 1 }, limit: 1 }).fetch()[0]; + return RoomHistoryManager.getSurroundingMessages(message, 50); + } + }, + + 'click .toggle-favorite'(event) { + event.stopPropagation(); + event.preventDefault(); + return Meteor.call('toggleFavorite', this._id, !$('i', event.currentTarget).hasClass('favorite-room'), function(err) { + if (err) { + return handleError(err); + } + }); + }, + + 'click .edit-room-title'(event) { + event.preventDefault(); + Session.set('editRoomTitle', true); + $('.fixed-title').addClass('visible'); + return Meteor.setTimeout(() => $('#room-title-field').focus().select() + , 10); + }, + + 'click .flex-tab .user-image > button'(e, instance) { + instance.tabBar.open(); + return instance.setUserDetail(this.user.username); + }, + + 'click .user-card-message'(e, instance) { + const roomData = Session.get(`roomData${ this._arguments[1].rid }`); + + if (RocketChat.Layout.isEmbedded()) { + fireGlobalEvent('click-user-card-message', { username: this._arguments[1].u.username }); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (['c', 'p', 'd'].includes(roomData.t)) { + instance.setUserDetail(this._arguments[1].u.username); + } + + instance.tabBar.setTemplate('membersList'); + return instance.tabBar.open(); + }, + + 'scroll .wrapper': _.throttle(function(e) { + if (RoomHistoryManager.isLoading(this._id) === false && RoomHistoryManager.hasMore(this._id) === true || RoomHistoryManager.hasMoreNext(this._id) === true) { + if (RoomHistoryManager.hasMore(this._id) === true && e.target.scrollTop === 0) { + return RoomHistoryManager.getMore(this._id); + } else if (RoomHistoryManager.hasMoreNext(this._id) === true && e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) { + return RoomHistoryManager.getMoreNext(this._id); + } + } + } + , 200), + + 'click .new-message'() { + Template.instance().atBottom = true; + return Template.instance().find('.input-message').focus(); + }, + + 'click .message-cog'() { + const [, message] = this._arguments; + RocketChat.MessageAction.hideDropDown(); + + let dropDown = $(`.messages-box \#${ message._id } .message-dropdown`); + + if (dropDown.length === 0) { + const actions = RocketChat.MessageAction.getButtons(message, 'message'); + + const el = Blaze.toHTMLWithData(Template.messageDropdown, + {actions}); + + $(`.messages-box \#${ message._id } .message-cog-container`).append(el); + + dropDown = $(`.messages-box \#${ message._id } .message-dropdown`); + } + + return dropDown.show(); + }, + + 'click .message-dropdown .message-action'(e, t) { + const el = $(e.currentTarget); + + const button = RocketChat.MessageAction.getButtonById(el.data('id')); + if ((button != null ? button.action : undefined) != null) { + return button.action.call(this, e, t); + } + }, + + 'click .message-dropdown-close'() { + return RocketChat.MessageAction.hideDropDown(); + }, + + 'click .mention-link'(e, instance) { + const channel = $(e.currentTarget).data('channel'); + if (channel != null) { + if (RocketChat.Layout.isEmbedded()) { + return fireGlobalEvent('click-mention-link', { path: FlowRouter.path('channel', {name: channel}), channel }); + } + + FlowRouter.go('channel', { name: channel }, FlowRouter.current().queryParams); + return; + } + + if (RocketChat.Layout.isEmbedded()) { + fireGlobalEvent('click-mention-link', { username: $(e.currentTarget).data('username') }); + e.stopPropagation(); + e.preventDefault(); + return; + } + + instance.tabBar.setTemplate('membersList'); + instance.setUserDetail($(e.currentTarget).data('username')); + + return instance.tabBar.open(); + }, + + 'click .image-to-download'(event) { + ChatMessage.update({_id: this._arguments[1]._id, 'urls.url': $(event.currentTarget).data('url')}, {$set: {'urls.$.downloadImages': true}}); + return ChatMessage.update({_id: this._arguments[1]._id, 'attachments.image_url': $(event.currentTarget).data('url')}, {$set: {'attachments.$.downloadImages': true}}); + }, + + 'click .collapse-switch'(e) { + const index = $(e.currentTarget).data('index'); + const collapsed = $(e.currentTarget).data('collapsed'); + const id = this._arguments[1]._id; + + if ((this._arguments[1] != null ? this._arguments[1].attachments : undefined) != null) { + ChatMessage.update({_id: id}, {$set: {[`attachments.${ index }.collapsed`]: !collapsed}}); + } + + if ((this._arguments[1] != null ? this._arguments[1].urls : undefined) != null) { + return ChatMessage.update({_id: id}, {$set: {[`urls.${ index }.collapsed`]: !collapsed}}); + } + }, + + 'dragenter .dropzone'(e) { + const types = e.originalEvent && e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.types; + if (types != null && types.length > 0 && _.every(types, type => type.indexOf('text/') === -1 || type.indexOf('text/uri-list') !== -1) && userCanDrop(this._id)) { + return e.currentTarget.classList.add('over'); + } + }, + + 'dragleave .dropzone-overlay'(e) { + return e.currentTarget.parentNode.classList.remove('over'); + }, + + 'dragover .dropzone-overlay'(e) { + e = e.originalEvent || e; + if (['move', 'linkMove'].includes(e.dataTransfer.effectAllowed)) { + return e.dataTransfer.dropEffect = 'move'; + } else { + return e.dataTransfer.dropEffect = 'copy'; + } + }, + + 'dropped .dropzone-overlay'(event) { + event.currentTarget.parentNode.classList.remove('over'); + + const e = event.originalEvent || event; + const files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || []; + + const filesToUpload = []; + for (const file of Array.from(files)) { + // `file.type = mime.lookup(file.name)` does not work. + Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); + filesToUpload.push({ + file, + name: file.name + }); + } + + return fileUpload(filesToUpload); + }, + + 'load img'(e, template) { + return (typeof template.sendToBottomIfNecessary === 'function' ? template.sendToBottomIfNecessary() : undefined); + }, + + 'click .jump-recent button'(e, template) { + e.preventDefault(); + template.atBottom = true; + return RoomHistoryManager.clear(template && template.data && template.data._id); + }, + + 'click .message'(e, template) { + if (template.selectable.get()) { + (document.selection != null ? document.selection.empty() : undefined) || (typeof window.getSelection === 'function' ? window.getSelection().removeAllRanges() : undefined); + const data = Blaze.getData(e.currentTarget); + const _id = data && data._arguments && data._arguments[1] && data._arguments[1]._id; + + if (!template.selectablePointer) { + template.selectablePointer = _id; + } + + if (!e.shiftKey) { + template.selectedMessages = template.getSelectedMessages(); + template.selectedRange = []; + template.selectablePointer = _id; + } + + template.selectMessages(_id); + + const selectedMessages = $('.messages-box .message.selected').map((i, message) => message.id); + const removeClass = _.difference(selectedMessages, template.getSelectedMessages()); + const addClass = _.difference(template.getSelectedMessages(), selectedMessages); + removeClass.forEach(message => $(`.messages-box #${ message }`).removeClass('selected')); + addClass.forEach(message => $(`.messages-box #${ message }`).addClass('selected')); + } + } +}); + + +Template.room.onCreated(function() { + // this.scrollOnBottom = true + // this.typing = new msgTyping this.data._id + this.showUsersOffline = new ReactiveVar(false); + this.atBottom = FlowRouter.getQueryParam('msg') ? false : true; + this.unreadCount = new ReactiveVar(0); + + this.selectable = new ReactiveVar(false); + this.selectedMessages = []; + this.selectedRange = []; + this.selectablePointer = null; + + this.flexTemplate = new ReactiveVar; + + this.userDetail = new ReactiveVar(FlowRouter.getParam('username')); + + this.tabBar = new RocketChatTabBar(); + this.tabBar.showGroup(FlowRouter.current().route.name); + + this.resetSelection = enabled => { + this.selectable.set(enabled); + $('.messages-box .message.selected').removeClass('selected'); + this.selectedMessages = []; + this.selectedRange = []; + return this.selectablePointer = null; + }; + + this.selectMessages = to => { + if ((this.selectablePointer === to) && (this.selectedRange.length > 0)) { + return this.selectedRange = []; + } else { + const message1 = ChatMessage.findOne(this.selectablePointer); + const message2 = ChatMessage.findOne(to); + + const minTs = _.min([message1.ts, message2.ts]); + const maxTs = _.max([message1.ts, message2.ts]); + + return this.selectedRange = _.pluck(ChatMessage.find({ rid: message1.rid, ts: { $gte: minTs, $lte: maxTs } }).fetch(), '_id'); + } + }; + + this.getSelectedMessages = () => { + let previewMessages; + const messages = this.selectedMessages; + let addMessages = false; + for (const message of Array.from(this.selectedRange)) { + if (messages.indexOf(message) === -1) { + addMessages = true; + break; + } + } + + if (addMessages) { + previewMessages = _.compact(_.uniq(this.selectedMessages.concat(this.selectedRange))); + } else { + previewMessages = _.compact(_.difference(this.selectedMessages, this.selectedRange)); + } + + return previewMessages; + }; + + this.setUserDetail = username => { + return this.userDetail.set(username); + }; + + this.clearUserDetail = () => { + return this.userDetail.set(null); + }; + + Meteor.call('getRoomRoles', this.data._id, function(error, results) { + if (error) { + return handleError(error); + } + + return Array.from(results).map((record) => { + delete record._id; + RoomRoles.upsert({ rid: record.rid, 'u._id': record.u._id }, record); + }); + }); + return RoomRoles.find({ rid: this.data._id }).observe({ + added: role => { + if (!role.u||!role.u._id) { + return; + } + return ChatMessage.update({ rid: this.data._id, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); + }, // Update message to re-render DOM + changed: (role) => { + if (!role.u||!role.u._id) { + return; + } + return ChatMessage.update({ rid: this.data._id, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); + }, // Update message to re-render DOM + removed: role => { + if (!role.u||!role.u._id) { + return; + } + return ChatMessage.update({ rid: this.data._id, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); + }}); +}); // Update message to re-render DOM + +Template.room.onDestroyed(function() { + return window.removeEventListener('resize', this.onWindowResize); +}); + +Template.room.onRendered(function() { + window.chatMessages = window.chatMessages || {}; + if (!window.chatMessages[Session.get('openedRoom')]) { + window.chatMessages[Session.get('openedRoom')] = new ChatMessages; + } + window.chatMessages[Session.get('openedRoom')].init(this.firstNode); + + if (Meteor.Device.isDesktop()) { + setTimeout(() => $('.message-form .input-message').focus() + , 100); + } + + // ScrollListener.init() + + const wrapper = this.find('.wrapper'); + const wrapperUl = this.find('.wrapper > ul'); + const newMessage = this.find('.new-message'); + + const template = this; + + const messageBox = $('.messages-box'); + + template.isAtBottom = function(scrollThreshold) { + if ((scrollThreshold == null)) { scrollThreshold = 0; } + if ((wrapper.scrollTop + scrollThreshold) >= (wrapper.scrollHeight - wrapper.clientHeight)) { + newMessage.className = 'new-message background-primary-action-color color-content-background-color not'; + return true; + } + return false; + }; + + template.sendToBottom = function() { + wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight; + return newMessage.className = 'new-message background-primary-action-color color-content-background-color not'; + }; + + template.checkIfScrollIsAtBottom = function() { + template.atBottom = template.isAtBottom(100); + readMessage.enable(); + return readMessage.read(); + }; + + template.sendToBottomIfNecessary = function() { + if ((template.atBottom === true) && (template.isAtBottom() !== true)) { + return template.sendToBottom(); + } + }; + + template.sendToBottomIfNecessaryDebounced = _.debounce(template.sendToBottomIfNecessary, 10); + + template.sendToBottomIfNecessary(); + + if ((window.MutationObserver == null)) { + wrapperUl.addEventListener('DOMSubtreeModified', () => template.sendToBottomIfNecessaryDebounced()); + } else { + const observer = new MutationObserver((mutations) => mutations.forEach(() => template.sendToBottomIfNecessaryDebounced())); + + observer.observe(wrapperUl, {childList: true}); + } + // observer.disconnect() + + template.onWindowResize = () => + Meteor.defer(() => template.sendToBottomIfNecessaryDebounced()) + ; + + window.addEventListener('resize', template.onWindowResize); + + wrapper.addEventListener('mousewheel', function() { + template.atBottom = false; + return Meteor.defer(() => template.checkIfScrollIsAtBottom()); + }); + + wrapper.addEventListener('wheel', function() { + template.atBottom = false; + return Meteor.defer(() => template.checkIfScrollIsAtBottom()); + }); + + wrapper.addEventListener('touchstart', () => template.atBottom = false); + + wrapper.addEventListener('touchend', function() { + Meteor.defer(() => template.checkIfScrollIsAtBottom()); + Meteor.setTimeout(() => template.checkIfScrollIsAtBottom() + , 1000); + return Meteor.setTimeout(() => template.checkIfScrollIsAtBottom() + , 2000); + }); + + wrapper.addEventListener('scroll', function() { + template.atBottom = false; + return Meteor.defer(() => template.checkIfScrollIsAtBottom()); + }); + + $('.flex-tab-bar').on('click', (/*e, t*/) => + Meteor.setTimeout(() => template.sendToBottomIfNecessaryDebounced() + , 50) + ); + + const rtl = $('html').hasClass('rtl'); + + const updateUnreadCount = _.throttle(function() { + let lastInvisibleMessageOnScreen; + const messageBoxOffset = messageBox.offset(); + + if (rtl) { + lastInvisibleMessageOnScreen = document.elementFromPoint((messageBoxOffset.left+messageBox.width())-1, messageBoxOffset.top+1); + } else { + lastInvisibleMessageOnScreen = document.elementFromPoint(messageBoxOffset.left+1, messageBoxOffset.top+1); + } + + if ((lastInvisibleMessageOnScreen != null ? lastInvisibleMessageOnScreen.id : undefined) != null) { + const lastMessage = ChatMessage.findOne(lastInvisibleMessageOnScreen.id); + if (lastMessage != null) { + const subscription = ChatSubscription.findOne({rid: template.data._id}); + const count = ChatMessage.find({rid: template.data._id, ts: {$lte: lastMessage.ts, $gt: (subscription != null ? subscription.ls : undefined)}}).count(); + return template.unreadCount.set(count); + } else { + return template.unreadCount.set(0); + } + } + } + , 300); + + readMessage.onRead(function(rid) { + if (rid === template.data._id) { + return template.unreadCount.set(0); + } + }); + + wrapper.addEventListener('scroll', () => updateUnreadCount()); + /* globals WebRTC */ + // salva a data da renderização para exibir alertas de novas mensagens + $.data(this.firstNode, 'renderedAt', new Date); + + const webrtc = WebRTC.getInstanceByRoomId(template.data._id); + if (webrtc != null) { + Tracker.autorun(() => { + const remoteItems = webrtc.remoteItems.get(); + if (remoteItems && remoteItems.length > 0) { + this.tabBar.setTemplate('membersList'); + this.tabBar.open(); + } + + if (webrtc.localUrl.get() != null) { + this.tabBar.setTemplate('membersList'); + return this.tabBar.open(); + } + }); + } +}); diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index 0a75485530..83b402db90 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -44,10 +44,10 @@ Package.onUse(function(api) { api.addFiles('client/lib/menu.js', 'client'); api.addFiles('client/lib/modal.coffee', 'client'); api.addFiles('client/lib/Modernizr.js', 'client'); - api.addFiles('client/lib/msgTyping.coffee', 'client'); - api.addFiles('client/lib/notification.coffee', 'client'); + api.addFiles('client/lib/msgTyping.js', 'client'); + api.addFiles('client/lib/notification.js', 'client'); api.addFiles('client/lib/parentTemplate.js', 'client'); - api.addFiles('client/lib/readMessages.coffee', 'client'); + api.addFiles('client/lib/readMessages.js', 'client'); api.addFiles('client/lib/rocket.coffee', 'client'); api.addFiles('client/lib/RoomHistoryManager.coffee', 'client'); api.addFiles('client/lib/RoomManager.coffee', 'client'); @@ -60,13 +60,13 @@ Package.onUse(function(api) { // LIB CORDOVA api.addFiles('client/lib/cordova/facebook-login.js', 'client'); api.addFiles('client/lib/cordova/keyboard-fix.js', 'client'); - api.addFiles('client/lib/cordova/push.coffee', 'client'); - api.addFiles('client/lib/cordova/urls.coffee', 'client'); + api.addFiles('client/lib/cordova/push.js', 'client'); + api.addFiles('client/lib/cordova/urls.js', 'client'); api.addFiles('client/lib/cordova/user-state.js', 'client'); // LIB RECORDERJS - api.addFiles('client/lib/recorderjs/audioRecorder.coffee', 'client'); - api.addFiles('client/lib/recorderjs/videoRecorder.coffee', 'client'); + api.addFiles('client/lib/recorderjs/audioRecorder.js', 'client'); + api.addFiles('client/lib/recorderjs/videoRecorder.js', 'client'); api.addFiles('client/lib/recorderjs/recorder.js', 'client'); // TEXTAREA CURSOR MANAGEMENT @@ -102,7 +102,7 @@ Package.onUse(function(api) { api.addFiles('client/views/app/home.js', 'client'); api.addFiles('client/views/app/mobileMessageMenu.js', 'client'); api.addFiles('client/views/app/privateHistory.js', 'client'); - api.addFiles('client/views/app/room.coffee', 'client'); + api.addFiles('client/views/app/room.js', 'client'); api.addFiles('client/views/app/roomSearch.js', 'client'); api.addFiles('client/views/app/secretURL.js', 'client'); api.addFiles('client/views/app/videoCall/videoButtons.js', 'client'); -- GitLab From 05391b9585177567732a33fb789b14f289a7db55 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 25 Apr 2017 14:46:34 -0300 Subject: [PATCH 020/280] Convert Ui Account Package to Js --- .../client/account.coffee | 4 - .../rocketchat-ui-account/client/account.js | 6 + .../client/accountFlex.coffee | 21 -- .../client/accountFlex.js | 27 ++ .../client/accountPreferences.coffee | 145 ----------- .../client/accountPreferences.js | 188 ++++++++++++++ .../client/accountProfile.coffee | 203 --------------- .../client/accountProfile.js | 240 ++++++++++++++++++ .../client/avatar/avatar.coffee | 14 - .../client/avatar/avatar.js | 15 ++ .../client/avatar/prompt.coffee | 105 -------- .../client/avatar/prompt.js | 136 ++++++++++ packages/rocketchat-ui-account/package.js | 13 +- 13 files changed, 618 insertions(+), 499 deletions(-) delete mode 100644 packages/rocketchat-ui-account/client/account.coffee create mode 100644 packages/rocketchat-ui-account/client/account.js delete mode 100644 packages/rocketchat-ui-account/client/accountFlex.coffee create mode 100644 packages/rocketchat-ui-account/client/accountFlex.js delete mode 100644 packages/rocketchat-ui-account/client/accountPreferences.coffee create mode 100644 packages/rocketchat-ui-account/client/accountPreferences.js delete mode 100644 packages/rocketchat-ui-account/client/accountProfile.coffee create mode 100644 packages/rocketchat-ui-account/client/accountProfile.js delete mode 100644 packages/rocketchat-ui-account/client/avatar/avatar.coffee create mode 100644 packages/rocketchat-ui-account/client/avatar/avatar.js delete mode 100644 packages/rocketchat-ui-account/client/avatar/prompt.coffee create mode 100644 packages/rocketchat-ui-account/client/avatar/prompt.js diff --git a/packages/rocketchat-ui-account/client/account.coffee b/packages/rocketchat-ui-account/client/account.coffee deleted file mode 100644 index 21a9874c24..0000000000 --- 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 0000000000..5673f50216 --- /dev/null +++ b/packages/rocketchat-ui-account/client/account.js @@ -0,0 +1,6 @@ +Template.account.onRendered(function() { + return Tracker.afterFlush(function() { + SideNav.setFlex('accountFlex'); + return 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 1e04bc7ce9..0000000000 --- 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 0000000000..7028a3f8ee --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountFlex.js @@ -0,0 +1,27 @@ +/*globals menu */ +Template.accountFlex.events({ + 'mouseenter header'() { + return SideNav.overArrow(); + }, + 'mouseleave header'() { + return SideNav.leaveArrow(); + }, + 'click header'() { + return SideNav.closeFlex(); + }, + 'click .cancel-settings'() { + return SideNav.closeFlex(); + }, + 'click .account-link'() { + return 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 5ed2003c65..0000000000 --- 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 0000000000..6e996392b4 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountPreferences.js @@ -0,0 +1,188 @@ +/*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).forEach((key) => { + const language = languages[key]; + result.push(_.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(); + let currentValue; + if (user && user.settings && user.settings.preferences && user.settings.preferences[property] && defaultValue === true) { + currentValue = value; + } else if (user && user.settings && user.settings.preferences && user.settings.preferences[property]) { + currentValue = !!user.settings.preferences[property]; + } + return currentValue === value; + }, + selected(property, value, defaultValue) { + const user = Meteor.user(); + if (!(user && user.settings && user.settings.preferences && user.settings.preferences[property])) { + return defaultValue === true; + } else { + return (user && user.settings && user.settings.preferences && user.settings.preferences[property]) === value; + } + }, + 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()) { + return Tracker.afterFlush(function() { + return $('#convertAsciiEmoji').show(); + }); + } else { + return Tracker.afterFlush(function() { + return $('#convertAsciiEmoji').hide(); + }); + } + }); + this.clearForm = function() { + return this.find('#language').value = localStorage.getItem('userLanguage'); + }; + return 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(); + return Meteor.call('saveUserPreferences', data, function(error, results) { + if (results) { + toastr.success(t('Preferences_saved')); + instance.clearForm(); + if (reload) { + setTimeout(function() { + return Meteor._reload.reload(); + }, 1000); + } + } + if (error) { + return handleError(error); + } + }); + }; +}); + +Template.accountPreferences.onRendered(function() { + return Tracker.afterFlush(function() { + SideNav.setFlex('accountFlex'); + return SideNav.openFlex(); + }); +}); + +Template.accountPreferences.events({ + 'click .submit button'(e, t) { + return t.save(); + }, + 'change input[name=useEmojis]'(e, t) { + return t.useEmojis.set($(e.currentTarget).val() === '1'); + }, + 'click .enable-notifications'() { + return KonchatNotification.getDesktopPermission(); + }, + 'click .test-notifications'() { + return 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 d1e0979cb5..0000000000 --- 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 0000000000..0db7192d3e --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountProfile.js @@ -0,0 +1,240 @@ +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() { + return 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() { + return customFields[this.name] = $(this).val() || ''; + }); + return 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() { + return Tracker.afterFlush(function() { + if (!RocketChat.settings.get('Accounts_AllowUserProfileChange')) { + FlowRouter.go('home'); + } + SideNav.setFlex('accountFlex'); + return 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(); + } + return 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') + }, () => { + return function(typedPassword) { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); + return 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'() { + return Meteor.logoutOtherClients(function(error) { + if (error) { + toastr.remove(); + return handleError(error); + } else { + toastr.remove(); + return 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)) { + return 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') + }, () => { + return function(typedPassword) { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + return Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { + if (error) { + toastr.remove(); + return swal.showInputError(t('Your_password_is_wrong')); + } else { + return swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } + }; + }); + } else { + return 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') + }, () => { + return function(deleteConfirmation) { + const user = Meteor.user(); + if (deleteConfirmation === (user && user.username)) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + return Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { + if (error) { + toastr.remove(); + return swal.showInputError(t('Your_password_is_wrong')); + } else { + return 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; + return Meteor.call('sendConfirmationEmail', user.emails && user.emails[0] && user.emails[0].address(() => { + return function(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 b0abe8754a..0000000000 --- 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 0000000000..cfee03985a --- /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 50f3d8838f..0000000000 --- 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 0000000000..8c607bf736 --- /dev/null +++ b/packages/rocketchat-ui-account/client/avatar/prompt.js @@ -0,0 +1,136 @@ +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); + return Meteor.call('getAvatarSuggestion', function(error, avatars) { + return self.suggestions.set({ + ready: true, + avatars + }); + }); + }; + return self.getSuggestions(); +}); + +Template.avatarPrompt.onRendered(function() { + return Tracker.afterFlush(function() { + if (!RocketChat.settings.get('Accounts_AllowUserAvatarChange')) { + FlowRouter.go('home'); + } + SideNav.setFlex('accountFlex'); + return 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'() { + let tmpService; + if (this.service === 'initials') { + return Meteor.call('resetAvatar', function(err) { + if (err && err.details.timeToReset && err.details.timeToReset) { + return toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.success(t('Avatar_changed_successfully')); + return RocketChat.callbacks.run('userAvatarSet', 'initials'); + } + }); + } else if (this.service === 'url') { + if (_.trim($('#avatarurl').val())) { + return Meteor.call('setAvatarFromService', $('#avatarurl').val(), '', this.service, function(err) { + if (err) { + if (err.details.timeToReset && err.details.timeToReset) { + return toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + return toastr.error(t('Avatar_url_invalid_or_error')); + } + } else { + toastr.success(t('Avatar_changed_successfully')); + return RocketChat.callbacks.run('userAvatarSet', 'url'); + } + }); + } else { + return toastr.error(t('Please_enter_value_for_url')); + } + } else { + tmpService = this.service; + return Meteor.call('setAvatarFromService', this.blob, this.contentType, this.service, function(err) { + if (err && err.details.timeToReset && err.details.timeToReset) { + return toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.success(t('Avatar_changed_successfully')); + return RocketChat.callbacks.run('userAvatarSet', tmpService); + } + }); + } + }, + 'click .login-with-service'(event, template) { + const loginWithService = `loginWith${ _.capitalize(this) }`; + const serviceConfig = {}; + return Meteor[loginWithService](serviceConfig, function(error) { + if ((error && error.error) === 'github-no-public-email') { + alert(t('github_no_public_email')); + return; + } + console.log(error); + if (error != null) { + toastr.error(error.message); + return; + } + return 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) || []; + } + console.log(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 + }); + return RocketChat.callbacks.run('userAvatarSet', 'upload'); + }; + }); + } +}); diff --git a/packages/rocketchat-ui-account/package.js b/packages/rocketchat-ui-account/package.js index 29966722f5..b461ce56a8 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'); }); -- GitLab From dae37ecb2b147a8b5133bb884e379c4f194a786a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 27 Apr 2017 15:08:38 -0300 Subject: [PATCH 021/280] conversion --- .../client/lib/chatMessages.coffee | 425 ------------------ .../rocketchat-ui/client/lib/chatMessages.js | 35 +- .../client/lib/cordova/push.coffee | 70 --- .../rocketchat-ui/client/lib/cordova/push.js | 6 +- .../client/lib/cordova/urls.coffee | 14 - .../rocketchat-ui/client/lib/cordova/urls.js | 4 +- .../rocketchat-ui/client/lib/msgTyping.coffee | 69 --- .../rocketchat-ui/client/lib/msgTyping.js | 2 +- .../client/lib/notification.coffee | 94 ---- .../rocketchat-ui/client/lib/notification.js | 7 +- .../client/lib/readMessages.coffee | 179 -------- .../lib/recorderjs/audioRecorder.coffee | 41 -- .../lib/recorderjs/videoRecorder.coffee | 78 ---- .../client/lib/recorderjs/videoRecorder.js | 12 +- packages/rocketchat-ui/package.js | 2 +- 15 files changed, 32 insertions(+), 1006 deletions(-) delete mode 100644 packages/rocketchat-ui/client/lib/chatMessages.coffee delete mode 100644 packages/rocketchat-ui/client/lib/cordova/push.coffee delete mode 100644 packages/rocketchat-ui/client/lib/cordova/urls.coffee delete mode 100644 packages/rocketchat-ui/client/lib/msgTyping.coffee delete mode 100644 packages/rocketchat-ui/client/lib/notification.coffee delete mode 100644 packages/rocketchat-ui/client/lib/readMessages.coffee delete mode 100644 packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.coffee delete mode 100644 packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.coffee diff --git a/packages/rocketchat-ui/client/lib/chatMessages.coffee b/packages/rocketchat-ui/client/lib/chatMessages.coffee deleted file mode 100644 index 42ddfe9948..0000000000 --- a/packages/rocketchat-ui/client/lib/chatMessages.coffee +++ /dev/null @@ -1,425 +0,0 @@ -import moment from 'moment' -import toastr from 'toastr' - -class @ChatMessages - init: (node) -> - this.editing = {} - this.records = {} - this.messageMaxSize = RocketChat.settings.get('Message_MaxAllowedSize') - this.wrapper = $(node).find(".wrapper") - this.input = $(node).find(".input-message").get(0) - this.$input = $(this.input) - this.hasValue = new ReactiveVar false - this.bindEvents() - return - - resize: -> - dif = (if RocketChat.Layout.isEmbedded() then 0 else 60) + $(".messages-container").find("footer").outerHeight() - dif += if $(".announcement").length > 0 then 40 else 0 - $(".messages-box").css - height: "calc(100% - #{dif}px)" - - getEditingIndex: (element) -> - msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)") - index = 0 - for msg in msgs - if msg is element - return index - index++ - return -1 - - recordInputAsDraft: () -> - id = this.editing.id - - message = this.getMessageById id - record = this.records[id] || {} - draft = this.input.value - - if(draft is message.msg) - this.clearCurrentDraft() - else - record.draft = draft - this.records[id] = record - - getMessageDraft: (id) -> - return this.records[id] - - clearMessageDraft: (id) -> - delete this.records[id] - - clearCurrentDraft: () -> - this.clearMessageDraft this.editing.id - - resetToDraft: (id) -> - message = this.getMessageById id - - old_value = this.input.value - this.input.value = message.msg - - return old_value isnt message.msg - - getMessageById: (id) -> - return ChatMessage.findOne(id) - - toPrevMessage: -> - index = this.editing.index - this.editByIndex if index? then index - 1 else undefined - - toNextMessage: -> - index = this.editing.index - this.clearEditing() unless this.editByIndex index + 1 - - editByIndex: (index) -> - return false if not this.editing.element and index? - - msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)") - index = msgs.length - 1 if not index? - - return false unless msgs[index] - - element = msgs[index] - this.edit element, index - - return true - - edit: (element, index) -> - index = this.getEditingIndex(element) if not index? - - message = this.getMessageById element.getAttribute("id") - - hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid) - editAllowed = RocketChat.settings.get 'Message_AllowEditing' - editOwn = message?.u?._id is Meteor.userId() - - return unless hasPermission or (editAllowed and editOwn) - return if element.classList.contains("system") - - blockEditInMinutes = RocketChat.settings.get 'Message_AllowEditing_BlockEditInMinutes' - if blockEditInMinutes? and blockEditInMinutes isnt 0 - msgTs = moment(message.ts) if message.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - if currentTsDiff > blockEditInMinutes - return - - msg = this.getMessageDraft(message._id)?.draft - msg = message.msg unless msg? - - editingNext = this.editing.index < index - - old_input = this.input.value - - this.clearEditing() - - this.hasValue.set true - this.editing.element = element - this.editing.index = index - this.editing.id = message._id - element.classList.add("editing") - this.input.classList.add("editing") - this.$input.closest('.message-form').addClass('editing') - - this.input.focus() - if message.attachments? and message.attachments[0].description? - this.input.value = message.attachments[0].description - else - this.input.value = msg - - cursor_pos = if editingNext then 0 else -1 - this.$input.setCursorPosition(cursor_pos) - - clearEditing: -> - if this.editing.element - this.recordInputAsDraft() - - this.editing.element.classList.remove("editing") - this.input.classList.remove("editing") - this.$input.closest('.message-form').removeClass('editing') - delete this.editing.id - delete this.editing.element - delete this.editing.index - - this.input.value = this.editing.saved or "" - cursor_pos = this.editing.savedCursor ? -1 - this.$input.setCursorPosition(cursor_pos) - - this.hasValue.set this.input.value isnt '' - else - this.editing.saved = this.input.value - this.editing.savedCursor = this.input.selectionEnd - - ###* - # * @param {string} rim room ID - # * @param {Element} input DOM element - # * @param {function?} done callback - ### - send: (rid, input, done = ->) -> - if _.trim(input.value) isnt '' - readMessage.enable() - readMessage.readNow() - $('.message.first-unread').removeClass('first-unread') - - msg = input.value - msgObject = { _id: Random.id(), rid: rid, msg: msg} - - # Run to allow local encryption, and maybe other client specific actions to be run before send - RocketChat.promises.run('onClientBeforeSendMessage', msgObject).then (msgObject) => - - # checks for the final msgObject.msg size before actually sending the message - if this.isMessageTooLong(msgObject.msg) - return toastr.error t('Message_too_long') - - this.clearCurrentDraft() - if this.editing.id - this.update(this.editing.id, rid, msgObject.msg) - return - - KonchatNotification.removeRoomNotification(rid) - input.value = '' - input.updateAutogrow?() - this.hasValue.set false - this.stopTyping(rid) - - #Check if message starts with /command - if msg[0] is '/' - match = msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m) - if match? - if RocketChat.slashCommands.commands[match[1]] - commandOptions = RocketChat.slashCommands.commands[match[1]] - command = match[1] - param = if match[2]? then match[2] else '' - if commandOptions.clientOnly - commandOptions.callback(command, param, msgObject) - else - Meteor.call 'slashCommand', {cmd: command, params: param, msg: msgObject }, (err, result) -> commandOptions.result?(err, result, {cmd: command, params: param, msg: msgObject }) - return - - if !RocketChat.settings.get('Message_AllowUnrecognizedSlashCommand') - invalidCommandMsg = - _id: Random.id() - rid: rid - ts: new Date - msg: TAPi18n.__('No_such_command', { command: match[1] }) - u: - username: "rocketbot" - private: true - ChatMessage.upsert { _id: invalidCommandMsg._id }, invalidCommandMsg - return - - Meteor.call 'sendMessage', msgObject - done() - - # If edited message was emptied we ask for deletion - else if this.editing.element - message = this.getMessageById this.editing.id - if message.attachments? and message.attachments[0].description? - this.update(this.editing.id, rid, '', true) - return - # Restore original message in textbox in case delete is canceled - this.resetToDraft this.editing.id - - this.confirmDeleteMsg message, done - - confirmDeleteMsg: (message, done = ->) -> - return if RocketChat.MessageTypes.isSystemMessage(message) - swal { - title: t('Are_you_sure') - text: t('You_will_not_be_able_to_recover') - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: t('Yes_delete_it') - cancelButtonText: t('Cancel') - closeOnConfirm: false - html: false - }, => - swal - title: t('Deleted') - text: t('Your_entry_has_been_deleted') - type: 'success' - timer: 1000 - showConfirmButton: false - - if this.editing.id is message._id - this.clearEditing message - this.deleteMsg message - - this.$input.focus() - done() - - # In order to avoid issue "[Callback not called when still animating](https://github.com/t4t5/sweetalert/issues/528)" - $('.sweet-alert').addClass 'visible' - - deleteMsg: (message) -> - blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes' - if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 - msgTs = moment(message.ts) if message.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - if currentTsDiff > blockDeleteInMinutes - toastr.error(t('Message_deleting_blocked')) - return - - Meteor.call 'deleteMessage', { _id: message._id }, (error, result) -> - if error - return handleError(error) - - pinMsg: (message) -> - message.pinned = true - Meteor.call 'pinMessage', message, (error, result) -> - if error - return handleError(error) - - unpinMsg: (message) -> - message.pinned = false - Meteor.call 'unpinMessage', message, (error, result) -> - if error - return handleError(error) - - update: (id, rid, msg, isDescription) -> - if _.trim(msg) isnt '' or isDescription is true - Meteor.call 'updateMessage', { _id: id, msg: msg, rid: rid } - this.clearEditing() - this.stopTyping(rid) - - startTyping: (rid, input) -> - if _.trim(input.value) isnt '' - MsgTyping.start(rid) - else - MsgTyping.stop(rid) - - stopTyping: (rid) -> - MsgTyping.stop(rid) - - bindEvents: -> - if this.wrapper?.length - $(".input-message").autogrow - postGrowCallback: => - this.resize() - - tryCompletion: (input) -> - value = input.value.match(/[^\s]+$/) - if value?.length > 0 - value = value[0] - - re = new RegExp value, 'i' - - user = Meteor.users.findOne username: re - if user? - input.value = input.value.replace value, "@#{user.username} " - - keyup: (rid, event) -> - input = event.currentTarget - k = event.which - keyCodes = [ - 13, # Enter - 20, # Caps lock - 16, # Shift - 9, # Tab - 27, # Escape Key - 17, # Control Key - 91, # Windows Command Key - 19, # Pause Break - 18, # Alt Key - 93, # Right Click Point Key - 45, # Insert Key - 34, # Page Down - 35, # Page Up - 144, # Num Lock - 145 # Scroll Lock - ] - keyCodes.push i for i in [35..40] # Home, End, Arrow Keys - keyCodes.push i for i in [112..123] # F1 - F12 - - unless k in keyCodes - this.startTyping(rid, input) - - this.hasValue.set input.value isnt '' - - keydown: (rid, event) -> - sendOnEnter = Meteor.user()?.settings?.preferences?.sendOnEnter - input = event.currentTarget - $input = $(input) - k = event.which - this.resize(input) - - if k is 13 - if not sendOnEnter? || sendOnEnter is 'normal' || (sendOnEnter is 'desktop' and Meteor.Device.isDesktop()) - if not event.shiftKey and not event.ctrlKey and not event.altKey and not event.metaKey # Enter without shift/ctrl/alt - event.preventDefault() - event.stopPropagation() - this.send(rid, input) - return - else if not event.shiftKey - return input.value +='\n' - else if sendOnEnter is 'alternative' - if event.shiftKey or event.ctrlKey or event.altKey or event.metaKey # Enter with shift/ctrl/alt - event.preventDefault() - event.stopPropagation() - this.send(rid, input) - return - - - - if k is 9 # Tab - event.preventDefault() - event.stopPropagation() - @tryCompletion input - - if k is 27 # Escape - if this.editing.index? - record = this.getMessageDraft(this.editing.id) - - # If resetting did nothing then edited message is same as original - unless this.resetToDraft this.editing.id - this.clearCurrentDraft() - this.clearEditing() - - event.preventDefault() - event.stopPropagation() - return - else if k is 38 or k is 40 # Arrow Up or down - return true if event.shiftKey - - cursor_pos = input.selectionEnd - - if k is 38 # Arrow Up - if cursor_pos is 0 - this.toPrevMessage() - else if not event.altKey - return true - - this.$input.setCursorPosition(0) if event.altKey - - else # Arrow Down - if cursor_pos is input.value.length - this.toNextMessage() - else if not event.altKey - return true - - this.$input.setCursorPosition(-1) if event.altKey - - return false - - # ctrl (command) + shift + k -> clear room messages - else if k is 75 and ((navigator?.platform?.indexOf('Mac') isnt -1 and event.metaKey and event.shiftKey) or (navigator?.platform?.indexOf('Mac') is -1 and event.ctrlKey and event.shiftKey)) - RoomHistoryManager.clear rid - - valueChanged: (rid, event) -> - if this.input.value.length is 1 - this.determineInputDirection() - - determineInputDirection: () -> - this.input.dir = if this.isMessageRtl(this.input.value) then 'rtl' else 'ltr' - - # http://stackoverflow.com/a/14824756 - isMessageRtl: (message) -> - ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF'+'\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF' - rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC' - rtlDirCheck = new RegExp "^[^#{ltrChars}]*[#{rtlChars}]" - - return rtlDirCheck.test message - - isMessageTooLong: (message) -> - message?.length > this.messageMaxSize - - isEmpty: -> - return !this.hasValue.get() diff --git a/packages/rocketchat-ui/client/lib/chatMessages.js b/packages/rocketchat-ui/client/lib/chatMessages.js index 488b3a1a4d..2644277545 100644 --- a/packages/rocketchat-ui/client/lib/chatMessages.js +++ b/packages/rocketchat-ui/client/lib/chatMessages.js @@ -83,21 +83,20 @@ this.ChatMessages = class ChatMessages { } editByIndex(index) { - if (!this.editing.element && (index != null)) { return false; } + if (!this.editing.element && index) { return false; } const msgs = this.wrapper.get(0).querySelectorAll('.own:not(.system)'); - if ((index == null)) { index = msgs.length - 1; } + if (index == null) { index = msgs.length - 1; } if (!msgs[index]) { return false; } const element = msgs[index]; this.edit(element, index); - return true; } edit(element, index) { - if ((index == null)) { index = this.getEditingIndex(element); } + index = index != null ? index : this.getEditingIndex(element); const message = this.getMessageById(element.getAttribute('id')); @@ -105,7 +104,7 @@ this.ChatMessages = class ChatMessages { const editAllowed = RocketChat.settings.get('Message_AllowEditing'); const editOwn = message && message.u && message.u._id === Meteor.userId(); - if (!hasPermission && (!editAllowed || !editOwn)) { return; } + if (!hasPermission && !editAllowed || !editOwn) { return; } if (element.classList.contains('system')) { return; } const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); @@ -121,7 +120,7 @@ this.ChatMessages = class ChatMessages { const draft = this.getMessageDraft(message._id); let msg = draft && draft.draft; - if (msg == null) { ({ msg } = message); } + msg = msg || message.msg; const editingNext = this.editing.index < index; @@ -138,7 +137,7 @@ this.ChatMessages = class ChatMessages { this.$input.closest('.message-form').addClass('editing'); this.input.focus(); - if ((message.attachments != null) && (message.attachments[0].description != null)) { + if (message.attachments && message.attachments[0].description) { this.input.value = message.attachments[0].description; } else { this.input.value = msg; @@ -175,8 +174,7 @@ this.ChatMessages = class ChatMessages { * * @param {Element} input DOM element * * @param {function?} done callback */ - send(rid, input, done) { - if (done == null) { done = function() {}; } + send(rid, input, done = function() {}) { if (_.trim(input.value) !== '') { readMessage.enable(); readMessage.readNow(); @@ -210,16 +208,16 @@ this.ChatMessages = class ChatMessages { //Check if message starts with /command if (msg[0] === '/') { const match = msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m); - if (match != null) { + if (match) { let command; if (RocketChat.slashCommands.commands[match[1]]) { const commandOptions = RocketChat.slashCommands.commands[match[1]]; command = match[1]; - const param = (match[2] != null) ? match[2] : ''; + const param = match[2] || ''; if (commandOptions.clientOnly) { commandOptions.callback(command, param, msgObject); } else { - Meteor.call('slashCommand', {cmd: command, params: param, msg: msgObject }, (err, result) => typeof commandOptions.result === 'function' ? commandOptions.result(err, result, {cmd: command, params: param, msg: msgObject }) : undefined); + Meteor.call('slashCommand', {cmd: command, params: param, msg: msgObject }, (err, result) => typeof commandOptions.result === 'function' && commandOptions.result(err, result, {cmd: command, params: param, msg: msgObject })); } return; } @@ -258,8 +256,7 @@ this.ChatMessages = class ChatMessages { } } - confirmDeleteMsg(message, done) { - if (done == null) { done = function() {}; } + confirmDeleteMsg(message, done = function() {}) { if (RocketChat.MessageTypes.isSystemMessage(message)) { return; } swal({ title: t('Are_you_sure'), @@ -352,7 +349,7 @@ this.ChatMessages = class ChatMessages { } bindEvents() { - if (this.wrapper != null ? this.wrapper.length : undefined) { + if (this.wrapper && this.wrapper.length) { return $('.input-message').autogrow({ postGrowCallback: () => { return this.resize(); @@ -366,7 +363,7 @@ this.ChatMessages = class ChatMessages { if (!value) { return; } const re = new RegExp(value, 'i'); const user = Meteor.users.findOne({username: re}); - if (user != null) { + if (user) { return input.value = input.value.replace(value, `@${ user.username } `); } } @@ -411,7 +408,7 @@ this.ChatMessages = class ChatMessages { this.resize(input); if (k === 13) { - if (sendOnEnter == null || sendOnEnter === 'normal' || (sendOnEnter === 'desktop' && Meteor.Device.isDesktop())) { + if (sendOnEnter == null || sendOnEnter === 'normal' || sendOnEnter === 'desktop' && Meteor.Device.isDesktop()) { if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { // Enter without shift/ctrl/alt event.preventDefault(); event.stopPropagation(); @@ -452,7 +449,7 @@ this.ChatMessages = class ChatMessages { event.stopPropagation(); return; } - } else if ((k === 38) || (k === 40)) { // Arrow Up or down + } else if (k === 38 || k === 40) { // Arrow Up or down if (event.shiftKey) { return true; } const cursor_pos = input.selectionEnd; @@ -504,7 +501,7 @@ this.ChatMessages = class ChatMessages { } isMessageTooLong(message) { - return (message != null ? message.length : undefined) > this.messageMaxSize; + return message && message.length > this.messageMaxSize; } isEmpty() { diff --git a/packages/rocketchat-ui/client/lib/cordova/push.coffee b/packages/rocketchat-ui/client/lib/cordova/push.coffee deleted file mode 100644 index 50406ab9dc..0000000000 --- a/packages/rocketchat-ui/client/lib/cordova/push.coffee +++ /dev/null @@ -1,70 +0,0 @@ -if Meteor.isCordova - # Push.addListener 'token', (token) -> - # Meteor.call 'log', 'CLIENT', 'token', arguments - - # Push.addListener 'error', (err) -> - # Meteor.call 'log', 'CLIENT', 'error', arguments - # if err.type == 'apn.cordova' - # Meteor.call 'log', 'CLIENT', err.error - - # Push.addListener 'register', (evt) -> - # Meteor.call 'log', 'CLIENT', 'register', arguments - - # Push.addListener 'alert', (notification) -> - # Meteor.call 'log', 'CLIENT', 'alert', arguments - - # Push.addListener 'sound', (notification) -> - # Meteor.call 'log', 'CLIENT', 'sound', arguments - - # Push.addListener 'badge', (notification) -> - # Meteor.call 'log', 'CLIENT', 'badge', arguments - - # Push.addListener 'message', (notification) -> - # Meteor.call 'log', 'CLIENT', 'message', arguments - - Push.addListener 'startup', (notification) -> - # Meteor.call 'log', 'CLIENT', 'startup', arguments - - if notification.payload?.rid? - if notification.payload.host is Meteor.absoluteUrl() - switch notification.payload.type - when 'c' - FlowRouter.go 'channel', { name: notification.payload.name }, FlowRouter.current().queryParams - when 'p' - FlowRouter.go 'group', { name: notification.payload.name }, FlowRouter.current().queryParams - when 'd' - FlowRouter.go 'direct', { username: notification.payload.sender.username }, FlowRouter.current().queryParams - else - path = '' - switch notification.payload.type - when 'c' - path = 'channel/' + notification.payload.name - when 'p' - path = 'group/' + notification.payload.name - when 'd' - path = 'direct/' + notification.payload.sender.username - - host = notification.payload.host.replace /\/$/, '' - if Servers.serverExists(host) isnt true - return - - Servers.startServer host, path, (err, url) -> - if err? - # TODO err - return console.log err - - - Meteor.startup -> - Tracker.autorun -> - if RocketChat.settings.get('Push_enable') is true - - Push.Configure - android: - senderID: window.ANDROID_SENDER_ID - sound: true - vibrate: true - ios: - badge: true - clearBadge: true - sound: true - alert: true diff --git a/packages/rocketchat-ui/client/lib/cordova/push.js b/packages/rocketchat-ui/client/lib/cordova/push.js index 7d322604d3..684c8cecfd 100644 --- a/packages/rocketchat-ui/client/lib/cordova/push.js +++ b/packages/rocketchat-ui/client/lib/cordova/push.js @@ -26,7 +26,7 @@ if (Meteor.isCordova) { Push.addListener('startup', function(notification) { // Meteor.call 'log', 'CLIENT', 'startup', arguments - if ((notification.payload != null ? notification.payload.rid : undefined) != null) { + if (notification.payload && notification.payload.rid) { if (notification.payload.host === Meteor.absoluteUrl()) { switch (notification.payload.type) { case 'c': @@ -56,7 +56,7 @@ if (Meteor.isCordova) { } return Servers.startServer(host, path, function(err) { - if (err != null) { + if (err) { // TODO err return console.log(err); } @@ -67,7 +67,7 @@ if (Meteor.isCordova) { Meteor.startup(() => - Tracker.autorun(function() { + Tracker.autorun(() => { if (RocketChat.settings.get('Push_enable') === true) { Push.Configure({ diff --git a/packages/rocketchat-ui/client/lib/cordova/urls.coffee b/packages/rocketchat-ui/client/lib/cordova/urls.coffee deleted file mode 100644 index aeed6e9604..0000000000 --- a/packages/rocketchat-ui/client/lib/cordova/urls.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Meteor.startup -> - return unless Meteor.isCordova - - # Handle click events for all external URLs - $(document).on 'deviceready', -> - platform = device.platform.toLowerCase() - $(document).on 'click', (e) -> - $link = $(e.target).closest('a[href]') - return unless $link.length > 0 - url = $link.attr('href') - - if /^https?:\/\/.+/.test(url) is true - window.open url, '_system' - e.preventDefault() diff --git a/packages/rocketchat-ui/client/lib/cordova/urls.js b/packages/rocketchat-ui/client/lib/cordova/urls.js index ffac42ed71..a11c61d810 100644 --- a/packages/rocketchat-ui/client/lib/cordova/urls.js +++ b/packages/rocketchat-ui/client/lib/cordova/urls.js @@ -1,7 +1,7 @@ -Meteor.startup(function() { +Meteor.startup(() => { if (!Meteor.isCordova) { return; } // Handle click events for all external URLs - $(document).on('deviceready', function() { + $(document).on('deviceready', () => { // const platform = device.platform.toLowerCase(); $(document).on('click', function(e) { const $link = $(e.target).closest('a[href]'); diff --git a/packages/rocketchat-ui/client/lib/msgTyping.coffee b/packages/rocketchat-ui/client/lib/msgTyping.coffee deleted file mode 100644 index b4355443fb..0000000000 --- a/packages/rocketchat-ui/client/lib/msgTyping.coffee +++ /dev/null @@ -1,69 +0,0 @@ -@MsgTyping = do -> - timeout = 15000 - timeouts = {} - renew = true - renewTimeout = 10000 - selfTyping = new ReactiveVar false - usersTyping = {} - dep = new Tracker.Dependency - - addStream = (room) -> - if _.isEmpty usersTyping[room]?.users - usersTyping[room] = { users: {} } - RocketChat.Notifications.onRoom room, 'typing', (username, typing) -> - unless username is Meteor.user()?.username - if typing is true - users = usersTyping[room].users - users[username] = Meteor.setTimeout -> - delete users[username] - usersTyping[room].users = users - dep.changed() - , timeout - usersTyping[room].users = users - dep.changed() - else - users = usersTyping[room].users - delete users[username] - usersTyping[room].users = users - dep.changed() - - Tracker.autorun -> - if Session.get 'openedRoom' - addStream Session.get 'openedRoom' - - start = (room) -> - return unless renew - - setTimeout -> - renew = true - , renewTimeout - - renew = false - selfTyping.set true - RocketChat.Notifications.notifyRoom room, 'typing', Meteor.user()?.username, true - clearTimeout timeouts[room] - timeouts[room] = Meteor.setTimeout -> - stop(room) - , timeout - - stop = (room) -> - renew = true - selfTyping.set false - if timeouts?[room]? - clearTimeout(timeouts[room]) - timeouts[room] = null - RocketChat.Notifications.notifyRoom room, 'typing', Meteor.user()?.username, false - - get = (room) -> - dep.depend() - unless usersTyping[room] - usersTyping[room] = { users: {} } - users = usersTyping[room].users - return _.keys(users) or [] - - return { - start: start - stop: stop - get: get - selfTyping: selfTyping - } diff --git a/packages/rocketchat-ui/client/lib/msgTyping.js b/packages/rocketchat-ui/client/lib/msgTyping.js index 73b388d93f..c1e6adde7f 100644 --- a/packages/rocketchat-ui/client/lib/msgTyping.js +++ b/packages/rocketchat-ui/client/lib/msgTyping.js @@ -36,7 +36,7 @@ export const MsgTyping = (function() { const stop = function(room) { renew = true; selfTyping.set(false); - if ((timeouts != null ? timeouts[room] : undefined) != null) { + if (timeouts && timeouts[room]) { clearTimeout(timeouts[room]); timeouts[room] = null; } diff --git a/packages/rocketchat-ui/client/lib/notification.coffee b/packages/rocketchat-ui/client/lib/notification.coffee deleted file mode 100644 index f3103eb717..0000000000 --- a/packages/rocketchat-ui/client/lib/notification.coffee +++ /dev/null @@ -1,94 +0,0 @@ -# @TODO implementar 'clicar na notificacao' abre a janela do chat -@KonchatNotification = - notificationStatus: new ReactiveVar - - # notificacoes HTML5 - getDesktopPermission: -> - if window.Notification && Notification.permission != "granted" && !Meteor.settings.public.sandstorm - Notification.requestPermission (status) -> - KonchatNotification.notificationStatus.set status - if Notification.permission != status - Notification.permission = status - - notify: (notification) -> - if window.Notification && Notification.permission == "granted" - message = { rid: notification.payload?.rid, msg: notification.text, notification: true } - RocketChat.promises.run('onClientMessageReceived', message).then (message) -> - n = new Notification notification.title, - icon: notification.icon or getAvatarUrlFromUsername notification.payload.sender.username - body: _.stripTags(message.msg) - tag: notification.payload._id, - silent: true - canReply: true - - notificationDuration = (notification.duration - 0) or (Meteor.user()?.settings?.preferences?.desktopNotificationDuration - 0) or RocketChat.settings.get('Desktop_Notifications_Duration') - if notificationDuration > 0 - setTimeout ( -> n.close() ), notificationDuration * 1000 - - if notification.payload?.rid? - if n.addEventListener? - n.addEventListener 'reply', ({response}) -> - Meteor.call 'sendMessage', - _id: Random.id() - rid: notification.payload.rid - msg: response - - n.onclick = -> - this.close() - window.focus() - switch notification.payload.type - when 'd' - FlowRouter.go 'direct', { username: notification.payload.sender.username }, FlowRouter.current().queryParams - when 'c' - FlowRouter.go 'channel', { name: notification.payload.name }, FlowRouter.current().queryParams - when 'p' - FlowRouter.go 'group', { name: notification.payload.name }, FlowRouter.current().queryParams - - showDesktop: (notification) -> - if notification.payload.rid is Session.get('openedRoom') and window.document.hasFocus?() - return - - if Meteor.user().status is 'busy' or Meteor.settings.public.sandstorm? - return - - getAvatarAsPng notification.payload.sender.username, (avatarAsPng) -> - notification.icon = avatarAsPng - KonchatNotification.notify(notification) - - newMessage: (rid) -> - if not Session.equals('user_' + Meteor.userId() + '_status', 'busy') - newMessageNotification = Meteor.user()?.settings?.preferences?.newMessageNotification || 'chime' - sub = ChatSubscription.findOne({ rid: rid }, { fields: { audioNotification: 1 } }); - if sub?.audioNotification isnt 'none' - if sub?.audioNotification - $("audio##{sub.audioNotification}")?[0]?.play?() - else if newMessageNotification isnt 'none' - $("audio##{newMessageNotification}")?[0]?.play?() - - newRoom: (rid, withSound = true) -> - Tracker.nonreactive -> - newRoomSound = Session.get('newRoomSound') - if newRoomSound? - newRoomSound = _.union newRoomSound, rid - else - newRoomSound = [rid] - - Session.set('newRoomSound', newRoomSound) - - # $('.link-room-' + rid).addClass('new-room-highlight') - - removeRoomNotification: (rid) -> - Tracker.nonreactive -> - Session.set('newRoomSound', []) - - $('.link-room-' + rid).removeClass('new-room-highlight') - -Tracker.autorun -> - newRoomNotification = Meteor.user()?.settings?.preferences?.newRoomNotification || 'door' - if Session.get('newRoomSound')?.length > 0 - Tracker.nonreactive -> - if not Session.equals('user_' + Meteor.userId() + '_status', 'busy') and newRoomNotification isnt 'none' - $("audio##{newRoomNotification}")?[0]?.play?() - else - $("audio##{newRoomNotification}")?[0]?.pause?() - $("audio##{newRoomNotification}")?[0]?.currentTime = 0 diff --git a/packages/rocketchat-ui/client/lib/notification.js b/packages/rocketchat-ui/client/lib/notification.js index 94c378ab32..095d181ba6 100644 --- a/packages/rocketchat-ui/client/lib/notification.js +++ b/packages/rocketchat-ui/client/lib/notification.js @@ -81,7 +81,7 @@ const KonchatNotification = { const user = Meteor.user(); const newMessageNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newMessageNotification || 'chime'; const sub = ChatSubscription.findOne({ rid }, { fields: { audioNotification: 1 } }); - if ((sub && sub.audioNotification) !== 'none') { + if (sub && sub.audioNotification !== 'none') { if (sub && sub.audioNotification) { const [audio] = $(`audio#${ sub.audioNotification }`); return audio && audio.play && audio.play(); @@ -93,8 +93,7 @@ const KonchatNotification = { } }, - newRoom(rid, withSound) { - if (withSound == null) { withSound = true; } + newRoom(rid/*, withSound = true*/) { Tracker.nonreactive(function() { let newRoomSound = Session.get('newRoomSound'); if (newRoomSound != null) { @@ -119,7 +118,7 @@ const KonchatNotification = { Tracker.autorun(function() { const user = Meteor.user(); const newRoomNotification = user && user.settings && user.settings.preferences && user.settings.preferences.newRoomNotification || 'door'; - if ((Session.get('newRoomSound') || []) > 0) { + if ((Session.get('newRoomSound') || []).length > 0) { Tracker.nonreactive(function() { if (!Session.equals(`user_${ Meteor.userId() }_status`, 'busy') && newRoomNotification !== 'none') { const [audio] = $(`audio#${ newRoomNotification }`); diff --git a/packages/rocketchat-ui/client/lib/readMessages.coffee b/packages/rocketchat-ui/client/lib/readMessages.coffee deleted file mode 100644 index 252c2c56cb..0000000000 --- a/packages/rocketchat-ui/client/lib/readMessages.coffee +++ /dev/null @@ -1,179 +0,0 @@ -### DEFINITIONS -- If window loses focus user needs to scroll or click/touch some place -- On hit ESC enable read, force read of current room and remove unread mark -- When user change room disable read until user interaction -- Only read if mark of *first-unread* is visible for user or if flag *force* was passed -- Always read the opened room -- The default method *read* has a delay of 2000ms to prevent multiple reads and to user be able to see the mark -### - -# Meteor.startup -> - # window.addEventListener 'focus', -> - # readMessage.refreshUnreadMark(undefined, true) - -@readMessage = new class - debug: false - - callbacks: [] - - constructor: -> - @canReadMessage = false - - readNow: (force=false) -> - console.log '--------------' if @debug - console.log 'readMessage -> readNow init process force:', force if @debug - - self = @ - - self.refreshUnreadMark() - - if force isnt true and @canReadMessage is false - console.log 'readMessage -> readNow canceled by canReadMessage: false' if @debug - return - - rid = Session.get 'openedRoom' - if not rid? - console.log 'readMessage -> readNow canceled, no rid informed' if @debug - return - - if force is true - console.log 'readMessage -> readNow via force rid:', rid if @debug - return Meteor.call 'readMessages', rid, -> - RoomHistoryManager.getRoom(rid).unreadNotLoaded.set 0 - self.refreshUnreadMark() - self.fireRead rid - - subscription = ChatSubscription.findOne rid: rid - if not subscription? - console.log 'readMessage -> readNow canceled, no subscription found for rid:', rid if @debug - return - - if subscription.alert is false and subscription.unread is 0 - console.log 'readMessage -> readNow canceled, alert', subscription.alert, 'and unread', subscription.unread if @debug - return - - room = RoomManager.getOpenedRoomByRid rid - if not room? - console.log 'readMessage -> readNow canceled, no room found for typeName:', subscription.t + subscription.name if @debug - return - - # Only read messages if user saw the first unread message - unreadMark = $('.message.first-unread') - if unreadMark.length > 0 - position = unreadMark.position() - visible = position?.top >= 0 - if not visible and room.unreadSince.get()? - console.log 'readMessage -> readNow canceled, unread mark visible:', visible, 'unread since exists', room.unreadSince.get()? if @debug - return - - console.log 'readMessage -> readNow rid:', rid if @debug - Meteor.call 'readMessages', rid, -> - RoomHistoryManager.getRoom(rid).unreadNotLoaded.set 0 - self.refreshUnreadMark() - self.fireRead rid - - read: _.debounce (force) -> - @readNow(force) - , 1000 - - disable: -> - @canReadMessage = false - - enable: -> - @canReadMessage = document.hasFocus() - - isEnable: -> - return @canReadMessage is true - - onRead: (cb) -> - @callbacks.push cb - - fireRead: (rid) -> - for cb in @callbacks - cb(rid) - - refreshUnreadMark: (rid, force) -> - self = @ - - rid ?= Session.get 'openedRoom' - if not rid? - return - - subscription = ChatSubscription.findOne rid: rid, {reactive: false} - if not subscription? - return - - room = RoomManager.openedRooms[subscription.t + subscription.name] - if not room? - return - - $roomDom = $(room.dom) - $roomDom.find('.message.first-unread').addClass('first-unread-opaque') - - if not subscription.alert and subscription.unread is 0 - room.unreadSince.set undefined - return - - if not force? and subscription.rid is Session.get('openedRoom') and document.hasFocus() - return - - $roomDom.find('.message.first-unread').removeClass('first-unread').removeClass('first-unread-opaque') - - lastReadRecord = ChatMessage.findOne - rid: subscription.rid - ts: - $lt: subscription.ls - # 'u._id': - # $ne: Meteor.userId() - , - sort: - ts: -1 - - if not lastReadRecord? and RoomHistoryManager.getRoom(room.rid).unreadNotLoaded.get() is 0 - lastReadRecord = - ts: new Date(0) - - if lastReadRecord? or RoomHistoryManager.getRoom(room.rid).unreadNotLoaded.get() > 0 - room.unreadSince.set subscription.ls - else - room.unreadSince.set undefined - - if lastReadRecord? - firstUnreadRecord = ChatMessage.findOne - rid: subscription.rid - ts: - $gt: lastReadRecord.ts - 'u._id': - $ne: Meteor.userId() - , - sort: - ts: 1 - - if firstUnreadRecord? - room.unreadFirstId = firstUnreadRecord._id - $roomDom.find('.message#'+firstUnreadRecord._id).addClass('first-unread') - - -Meteor.startup -> - $(window).on 'blur', -> - readMessage.disable() - - $(window).on 'focus', -> - readMessage.enable() - readMessage.read() - - $(window).on 'click', (e) -> - readMessage.enable() - readMessage.read() - - $(window).on 'touchend', (e) -> - readMessage.enable() - readMessage.read() - - $(window).on 'keyup', (e) -> - key = e.which - - if key is 27 - readMessage.enable() - readMessage.readNow(true) - $('.message.first-unread').removeClass('first-unread') diff --git a/packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.coffee b/packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.coffee deleted file mode 100644 index e51dbeff7a..0000000000 --- a/packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.coffee +++ /dev/null @@ -1,41 +0,0 @@ -@AudioRecorder = new class - start: (cb) -> - window.AudioContext = window.AudioContext or window.webkitAudioContext - navigator.getUserMedia = navigator.getUserMedia or navigator.webkitGetUserMedia - window.URL = window.URL or window.webkitURL - - window.audioContext = new AudioContext - - ok = (stream) => - @startUserMedia(stream) - cb?.call(@) - - if not navigator.getUserMedia? - return cb false - - navigator.getUserMedia {audio: true}, ok, (e) -> - console.log('No live audio input: ' + e) - - startUserMedia: (stream) -> - @stream = stream - input = window.audioContext.createMediaStreamSource(stream) - @recorder = new Recorder(input, {workerPath: '/recorderWorker.js'}) - @recorder.record() - - stop: (cb) -> - @recorder.stop() - - if cb? - @getBlob cb - - @stream.getAudioTracks()[0].stop() - - @recorder.clear() - - window.audioContext.close() - delete window.audioContext - delete @recorder - delete @stream - - getBlob: (cb) -> - @recorder.exportWAV cb diff --git a/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.coffee b/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.coffee deleted file mode 100644 index 062694df7a..0000000000 --- a/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.coffee +++ /dev/null @@ -1,78 +0,0 @@ -@VideoRecorder = new class - started: false - cameraStarted: new ReactiveVar false - recording: new ReactiveVar false - recordingAvailable: new ReactiveVar false - - start: (videoel, cb) -> - navigator.getUserMedia = navigator.getUserMedia or navigator.webkitGetUserMedia or navigator.mozGetUserMedia - window.URL = window.URL or window.webkitURL - - @videoel = videoel - ok = (stream) => - @startUserMedia(stream) - cb?.call(@) - - if not navigator.getUserMedia? - return cb false - - navigator.getUserMedia {audio: true, video: true}, ok, (e) -> - console.log('No live video input: ' + e) - - record: -> - @chunks = [] - if not @stream? - return - @mediaRecorder = new MediaRecorder(@stream) - @mediaRecorder.stream = @stream - @mediaRecorder.mimeType = 'video/webm' - @mediaRecorder.ondataavailable = (blobev) => - @chunks.push(blobev.data) - if not @recordingAvailable.get() - @recordingAvailable.set true - @mediaRecorder.start() - @recording.set true - - startUserMedia: (stream) -> - @stream = stream - @videoel.src = URL.createObjectURL(stream) - @videoel.onloadedmetadata = (e) => - @videoel.play() - - @started = true - @cameraStarted.set true - - stop: (cb) -> - if @started - @stopRecording() - - if @stream? - vtracks = @stream.getVideoTracks() - for vtrack in vtracks - vtrack.stop() - - atracks = @stream.getAudioTracks() - for atrack in atracks - atrack.stop() - - if @videoel? - @videoel.pause - @videoel.src = '' - - @started = false - @cameraStarted.set false - @recordingAvailable.set false - - if cb? and @chunks? - blob = new Blob(@chunks, { 'type' : 'video/webm' }) - cb(blob) - - delete @recorder - delete @stream - delete @videoel - - stopRecording: -> - if @started and @recording and @mediaRecorder? - @mediaRecorder.stop() - @recording.set false - delete @mediaRecorder diff --git a/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js b/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js index 8ae2be4851..3e2c82f7c9 100644 --- a/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js +++ b/packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js @@ -16,7 +16,7 @@ this.VideoRecorder = new class { return (cb != null ? cb.call(this) : undefined); }; - if ((navigator.getUserMedia == null)) { + if (navigator.getUserMedia == null) { return cb(false); } @@ -25,7 +25,7 @@ this.VideoRecorder = new class { record() { this.chunks = []; - if ((this.stream == null)) { + if (this.stream == null) { return; } this.mediaRecorder = new MediaRecorder(this.stream); @@ -56,7 +56,7 @@ this.VideoRecorder = new class { if (this.started) { this.stopRecording(); - if (this.stream != null) { + if (this.stream) { const vtracks = this.stream.getVideoTracks(); for (const vtrack of Array.from(vtracks)) { vtrack.stop(); @@ -68,7 +68,7 @@ this.VideoRecorder = new class { } } - if (this.videoel != null) { + if (this.videoel) { this.videoel.pause; this.videoel.src = ''; } @@ -77,7 +77,7 @@ this.VideoRecorder = new class { this.cameraStarted.set(false); this.recordingAvailable.set(false); - if ((cb != null) && (this.chunks != null)) { + if (cb && this.chunks) { const blob = new Blob(this.chunks, { 'type' : 'video/webm' }); cb(blob); } @@ -89,7 +89,7 @@ this.VideoRecorder = new class { } stopRecording() { - if (this.started && this.recording && this.mediaRecorder != null) { + if (this.started && this.recording && this.mediaRecorder) { this.mediaRecorder.stop(); this.recording.set(false); return delete this.mediaRecorder; diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index 83b402db90..f9ad4248d5 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -35,7 +35,7 @@ Package.onUse(function(api) { api.addFiles('client/lib/accountBox.js', 'client'); api.addFiles('client/lib/accounts.js', 'client'); api.addFiles('client/lib/avatar.js', 'client'); - api.addFiles('client/lib/chatMessages.coffee', 'client'); + api.addFiles('client/lib/chatMessages.js', 'client'); api.addFiles('client/lib/collections.js', 'client'); api.addFiles('client/lib/customEventPolyfill.js', 'client'); api.addFiles('client/lib/fileUpload.coffee', 'client'); -- GitLab From d40d639d47af31f554a9c1cf1c340a8880ce9214 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 27 Apr 2017 18:14:33 -0300 Subject: [PATCH 022/280] Add GridFS support to avatars --- lib/fileUpload.js | 73 +++++++++++++++++++ .../client/lib/FileUploadGridFS.js | 36 +++------ .../client/avatar/prompt.coffee | 2 +- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/lib/fileUpload.js b/lib/fileUpload.js index fbb006f15b..8f26103319 100644 --- a/lib/fileUpload.js +++ b/lib/fileUpload.js @@ -72,6 +72,79 @@ if (UploadFS) { return true; } }); + + Meteor.fileStoreAvatar = new UploadFS.store.GridFS({ + collection: RocketChat.models.Uploads.model, + name: 'rocketchat_uploads_avatar', + collectionName: 'rocketchat_uploads', + // filter: new UploadFS.Filter({ + // onCheck: FileUpload.validateFileUpload + // }), + beforeSave() { + console.log('beforeSave ->', arguments); + }, + beforeWrite() { + console.log('beforeWrite ->', arguments); + return { + username: 'testando' + }; + }, + transformWrite(readStream, writeStream, fileId, file) { + if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { + return readStream.pipe(writeStream); + } + + let stream = undefined; + + const identify = function(err, data) { + if (err) { + return stream.pipe(writeStream); + } + + file.identify = { + format: data.format, + size: data.size + }; + + if (data.Orientation && !['', 'Unknown', 'Undefined'].includes(data.Orientation)) { + RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); + } else { + stream.pipe(writeStream); + } + }; + + stream = RocketChatFile.gm(readStream).identify(identify).stream(); + }, + + onRead(fileId, file, req, res) { + if (RocketChat.settings.get('FileUpload_ProtectFiles')) { + let uid; + let token; + + if (req && req.headers && req.headers.cookie) { + const rawCookies = req.headers.cookie; + + if (rawCookies) { + uid = cookie.get('rc_uid', rawCookies) ; + token = cookie.get('rc_token', rawCookies); + } + } + + if (!uid) { + uid = req.query.rc_uid; + token = req.query.rc_token; + } + + if (!uid || !token || !RocketChat.models.Users.findOneByIdAndLoginToken(uid, token)) { + res.writeHead(403); + return false; + } + } + + res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`); + return true; + } + }); }; Meteor.startup(function() { diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js index ac14bb7faf..a952090d87 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js @@ -1,38 +1,24 @@ /* globals FileUploadBase, UploadFS, FileUpload:true */ FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { - constructor(meta, file) { + constructor(directive, meta, file) { super(meta, file); + this.store = directive === 'avatar' ? Meteor.fileStoreAvatar : Meteor.fileStore; + } + + start(callback) { this.handler = new UploadFS.Uploader({ - store: Meteor.fileStore, - data: file, - file: meta, + store: this.store, + data: this.file, + file: this.meta, onError: (err) => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - if (item != null) { - item.error = err.reason; - item.percentage = 0; - } - return Session.set('uploading', uploading); - } + return callback(err); }, onComplete: (fileData) => { const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); - file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } + return callback(null, file); }, 2000); }); } @@ -41,9 +27,7 @@ FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { this.handler.onProgress = (file, progress) => { this.onProgress(progress); }; - } - start() { return this.handler.start(); } diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.coffee b/packages/rocketchat-ui-account/client/avatar/prompt.coffee index 82fa805444..17f255c3cb 100644 --- a/packages/rocketchat-ui-account/client/avatar/prompt.coffee +++ b/packages/rocketchat-ui-account/client/avatar/prompt.coffee @@ -75,7 +75,7 @@ Template.avatarPrompt.events type: files[0].type # description: document.getElementById('file-description').value - upload = fileUploadHandler 'rocketchat-avatars-gs', record, files[0] + upload = fileUploadHandler 'avatar', record, files[0] # upload.onProgress = (progress) -> # console.log 'progress ->', progress -- GitLab From 4785fd479a3dfb87625c8cedc3b9dfabc7b9bfc6 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 28 Apr 2017 10:20:56 -0300 Subject: [PATCH 023/280] Better handling for GridFS avatars --- lib/fileUpload.js | 21 +- .../client/lib/FileUploadGridFS.js | 4 + .../server/config/configFileUploadAmazonS3.js | 2 +- .../config/configFileUploadGoogleStorage.js | 2 +- .../server/config/configFileUploadGridFS.js | 28 +++ .../server/models/Uploads.coffee | 23 +- server/methods/setAvatarFromService.js | 33 ++- server/startup/avatar.js | 219 +++++------------- 8 files changed, 153 insertions(+), 179 deletions(-) diff --git a/lib/fileUpload.js b/lib/fileUpload.js index 8f26103319..f466da9ceb 100644 --- a/lib/fileUpload.js +++ b/lib/fileUpload.js @@ -80,15 +80,6 @@ if (UploadFS) { // filter: new UploadFS.Filter({ // onCheck: FileUpload.validateFileUpload // }), - beforeSave() { - console.log('beforeSave ->', arguments); - }, - beforeWrite() { - console.log('beforeWrite ->', arguments); - return { - username: 'testando' - }; - }, transformWrite(readStream, writeStream, fileId, file) { if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { return readStream.pipe(writeStream); @@ -116,6 +107,18 @@ if (UploadFS) { stream = RocketChatFile.gm(readStream).identify(identify).stream(); }, + onFinishUpload(file) { + // update file record to match user's username + const user = RocketChat.models.Users.findOneById(file.userId); + const oldAvatar = RocketChat.models.Uploads.findOneByName(`${ user.username }.avatar`); + if (oldAvatar) { + Meteor.fileStoreAvatar.delete(oldAvatar._id); + RocketChat.models.Uploads.deleteFile(oldAvatar._id); + } + RocketChat.models.Uploads.updateFileNameById(file._id, `${ user.username }.avatar`); + // console.log('upload finished ->', file); + }, + onRead(fileId, file, req, res) { if (RocketChat.settings.get('FileUpload_ProtectFiles')) { let uid; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js index a952090d87..d5236b1c97 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js @@ -2,6 +2,7 @@ FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { constructor(directive, meta, file) { super(meta, file); + this.directive = directive; this.store = directive === 'avatar' ? Meteor.fileStoreAvatar : Meteor.fileStore; } @@ -15,6 +16,9 @@ FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { }, onComplete: (fileData) => { const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); + if (this.directive === 'avatar') { + return callback(null, file); + } file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { Meteor.setTimeout(() => { diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index 284edb1992..95d1dcd843 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -105,7 +105,7 @@ const configureSlingshot = _.debounce(() => { path } }; - RocketChat.models.Uploads.insertFileInitByUsername(user.username, this.userId, 's3', file, upload); + RocketChat.models.Uploads.insertAvatarFileInit(user.username, this.userId, 's3', file, upload); return path + user.username; } diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js index b7ae3dfa45..b2d1c6b9a8 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js @@ -121,7 +121,7 @@ const createGoogleStorageDirective = _.debounce(() => { path } }; - RocketChat.models.Uploads.insertFileInitByUsername(user.username, this.userId, 'googleCloudStorage', file, upload); + RocketChat.models.Uploads.insertAvatarFileInit(user.username, this.userId, 'googleCloudStorage', file, upload); return path + user.username; } diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js b/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js index 21ac212db8..fad4015518 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js @@ -139,3 +139,31 @@ FileUpload.addHandler('rocketchat_uploads', { return Meteor.fileStore.delete(file._id); } }); + +FileUpload.addHandler('rocketchat_uploads_avatar', { + get(file, req, res) { + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + file = FileUpload.addExtensionTo(file); + const headers = { + 'Cache-Control': 'public, max-age=0', + 'Expires': '-1', + 'Content-Disposition': 'inline', + 'Last-Modified': file.uploadedAt.toUTCString(), + 'Content-Type': file.type, + 'Content-Length': file.size + }; + return readFromGridFS(file.store, file._id, file, headers, req, res); + }, + delete(file) { + return Meteor.fileStore.delete(file._id); + } +}); diff --git a/packages/rocketchat-lib/server/models/Uploads.coffee b/packages/rocketchat-lib/server/models/Uploads.coffee index 2604403bbe..6fc165a190 100644 --- a/packages/rocketchat-lib/server/models/Uploads.coffee +++ b/packages/rocketchat-lib/server/models/Uploads.coffee @@ -48,9 +48,9 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return file - insertFileInitByUsername: (username, userId, store, file, extra) -> + insertAvatarFileInit: (name, userId, store, file, extra) -> fileData = - _id: username + name: "#{name}.avatar" userId: userId store: store complete: false @@ -88,12 +88,13 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return result - updateFileCompleteByUsername: (username, userId, url) -> - if not username + # @TODO deprecated + updateFileCompleteByNameAndUserId: (name, userId, url) -> + if not name return filter = - username: username + name: name userId: userId update = @@ -110,11 +111,19 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return result - findOneByUsername: (username) -> - return @findOne username: username + findOneByName: (name) -> + return @findOne name: name deleteFile: (fileId) -> if @model.direct?.remove? return @model.direct.remove { _id: fileId } else return @remove { _id: fileId } + + updateFileNameById: (fileId, name) -> + filter = _id: fileId + update = $set: name: name + if @model.direct?.update? + return @model.direct.update filter, update + else + return @update filter, update diff --git a/server/methods/setAvatarFromService.js b/server/methods/setAvatarFromService.js index bdc0768552..8781e1c450 100644 --- a/server/methods/setAvatarFromService.js +++ b/server/methods/setAvatarFromService.js @@ -22,10 +22,37 @@ Meteor.methods({ }, saveAvatarFile(file) { + check(file, Match.ObjectIncluding({ + _id: String + })); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'setAvatarFromService' + }); + } + + if (!RocketChat.settings.get('Accounts_AllowUserAvatarChange')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'setAvatarFromService' + }); + } + const user = RocketChat.models.Users.findOneById(Meteor.userId()); - // console.log('user ->', user); - // console.log('file ->', file); - RocketChat.models.Uploads.updateFileCompleteByUsername(user.username, Meteor.userId(), file.url); + const fileSaved = RocketChat.models.Uploads.findOneById(file._id); + + if (fileSaved.userId !== user._id) { + // this file is not user's avatar + throw new Meteor.Error('invalid-avatar'); + } + + // just returns if file already complete (case for GridFS) + if (fileSaved.complete) { + return true; + } + + RocketChat.models.Uploads.updateFileCompleteByNameAndUserId(`${ user.username }.avatar`, Meteor.userId(), file.url); + return true; } }); diff --git a/server/startup/avatar.js b/server/startup/avatar.js index 634effe061..cf31fcc236 100644 --- a/server/startup/avatar.js +++ b/server/startup/avatar.js @@ -1,5 +1,4 @@ -import getFileType from 'file-type'; - +/* globals FileUpload */ Meteor.startup(function() { let storeType = 'GridFS'; @@ -47,183 +46,87 @@ Meteor.startup(function() { return; } - return getNewAvatar(params, req, res); - })); -}); - - -function getNewAvatar(params, req, res) { - const match = /^\/([^?]*)/.exec(req.url); - - if (match[1]) { - let username = decodeURIComponent(match[1]); - const file = RocketChat.models.Uploads.findOneByUsername(username); - - if (file) { - res.setHeader('Content-Security-Policy', 'default-src \'none\''); + const match = /^\/([^?]*)/.exec(req.url); - return FileUpload.get(file, req, res); - } else { - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); + if (match[1]) { + let username = decodeURIComponent(match[1]); + let file; - const reqModifiedHeader = req.headers['if-modified-since']; - - if (reqModifiedHeader) { - if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { - res.writeHead(304); - res.end(); - return; - } - } - - const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; - - if (RocketChat.settings.get('UI_Use_Name_Avatar')) { - const user = RocketChat.models.Users.findOneByUsername(username, { - fields: { - name: 1 + if (username[0] !== '@') { + if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm) { + const user = RocketChat.models.Users.findOneByUsername(username); + if (user && user.services && user.services.sandstorm && user.services.sandstorm.picture) { + res.setHeader('Location', user.services.sandstorm.picture); + res.writeHead(302); + res.end(); + return; } - }); - - if (user && user.name) { - username = user.name; } + file = RocketChat.models.Uploads.findOneByName(`${ username }.avatar`); } - let color = ''; - let initials = ''; + if (file) { + res.setHeader('Content-Security-Policy', 'default-src \'none\''); - if (username === '?') { - color = '#000'; - initials = username; + return FileUpload.get(file, req, res); } else { - const position = username.length % colors.length; - - color = colors[position]; - username = username.replace(/[^A-Za-z0-9]/g, '.').replace(/\.+/g, '.').replace(/(^\.)|(\.$)/g, ''); + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); + + const reqModifiedHeader = req.headers['if-modified-since']; + + if (reqModifiedHeader) { + if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { + res.writeHead(304); + res.end(); + return; + } + } - const usernameParts = username.split('.'); + const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; - initials = usernameParts.length > 1 ? _.first(usernameParts)[0] + _.last(usernameParts)[0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); - initials = initials.toUpperCase(); - } + if (RocketChat.settings.get('UI_Use_Name_Avatar')) { + const user = RocketChat.models.Users.findOneByUsername(username, { + fields: { + name: 1 + } + }); - const svg = `\n\n \n ${ initials }\n \n`; - res.write(svg); - res.end(); + if (user && user.name) { + username = user.name; + } + } - return; - } - } + let color = ''; + let initials = ''; - res.writeHead(404); - res.end(); - return; -} - -function getOldAvatar(params, req, res) { - let username = params.username.replace(/\.jpg$/, '').replace(/^@/, ''); - let file; - if (params.username[0] !== '@') { - if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm) { - const user = RocketChat.models.Users.findOneByUsername(username); - if (user && user.services && user.services.sandstorm && user.services.sandstorm.picture) { - res.setHeader('Location', user.services.sandstorm.picture); - res.writeHead(302); - res.end(); - return; - } - } + if (username === '?') { + color = '#000'; + initials = username; + } else { + const position = username.length % colors.length; - file = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ username }.jpg`)); - } + color = colors[position]; + username = username.replace(/[^A-Za-z0-9]/g, '.').replace(/\.+/g, '.').replace(/(^\.)|(\.$)/g, ''); - res.setHeader('Content-Disposition', 'inline'); - if (!file) { - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); + const usernameParts = username.split('.'); - const reqModifiedHeader = req.headers['if-modified-since']; + initials = usernameParts.length > 1 ? _.first(usernameParts)[0] + _.last(usernameParts)[0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); + initials = initials.toUpperCase(); + } - if (reqModifiedHeader) { - if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { - res.writeHead(304); + const svg = `\n\n \n ${ initials }\n \n`; + res.write(svg); res.end(); - return; - } - } - - const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; - - if (RocketChat.settings.get('UI_Use_Name_Avatar')) { - const user = RocketChat.models.Users.findOneByUsername(username, { - fields: { - name: 1 - } - }); - if (user && user.name) { - username = user.name; + return; } } - let color = ''; - let initials = ''; - - if (username === '?') { - color = '#000'; - initials = username; - } else { - const position = username.length % colors.length; - - color = colors[position]; - username = username.replace(/[^A-Za-z0-9]/g, '.').replace(/\.+/g, '.').replace(/(^\.)|(\.$)/g, ''); - - const usernameParts = username.split('.'); - - initials = usernameParts.length > 1 ? _.first(usernameParts)[0] + _.last(usernameParts)[0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); - initials = initials.toUpperCase(); - } - - const svg = `\n\n \n ${ initials }\n \n`; - res.write(svg); + res.writeHead(404); res.end(); - return; - } - - const reqModifiedHeader = req.headers['if-modified-since']; - if (reqModifiedHeader) { - if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { - res.setHeader('Last-Modified', reqModifiedHeader); - res.writeHead(304); - res.end(); - return; - } - } - - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); - res.setHeader('Content-Length', file.length); - - if (file.contentType) { - res.setHeader('Content-Type', file.contentType); - } else { - file.readStream.once('data', function(chunk) { - const fileType = getFileType(chunk); - if (fileType) { - return res.setHeader('Content-Type', fileType.mime); - } else { - return res.setHeader('Content-Type', 'image/jpeg'); - } - }); - } - - file.readStream.pipe(res); -} + })); +}); -- GitLab From 4a6d647fabcf62bc9d20b3b87a507e25ad3839a0 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 28 Apr 2017 17:03:16 -0300 Subject: [PATCH 024/280] ui admin wip --- .../rocketchat-ui-admin/client/admin.coffee | 1 + packages/rocketchat-ui-admin/client/admin.js | 698 ++++++++++++++++++ 2 files changed, 699 insertions(+) create mode 100644 packages/rocketchat-ui-admin/client/admin.js diff --git a/packages/rocketchat-ui-admin/client/admin.coffee b/packages/rocketchat-ui-admin/client/admin.coffee index 3abde676b3..8e8fe21469 100644 --- a/packages/rocketchat-ui-admin/client/admin.coffee +++ b/packages/rocketchat-ui-admin/client/admin.coffee @@ -126,6 +126,7 @@ Template.admin.helpers enableQuery = [enableQuery] found = 0 + console.log enableQuery for item in enableQuery if TempSettings.findOne(item)? found++ diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js new file mode 100644 index 0000000000..7694345596 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -0,0 +1,698 @@ +/*globals jscolor, i18nDefaultQuery */ +import toastr from 'toastr'; +const TempSettings = new Mongo.Collection(null); + +RocketChat.TempSettings = TempSettings; + +const getDefaultSetting = function(settingId) { + return RocketChat.settings.collectionPrivate.findOne({ + _id: settingId + }); +}; + +const setFieldValue = function(settingId, value, type, editor) { + const input = $('.page-settings').find(`[name="${ settingId }"]`); + switch (type) { + case 'boolean': + return $('.page-settings').find(`[name="${ settingId }"][value="${ Number(value) }"]`).prop('checked', true).change(); + case 'code': + return input.next()[0].CodeMirror.setValue(value); + case 'color': + input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change(); + input.val(value).change(); + if (editor === 'color') { + return new jscolor(input); + } + break; + default: + return input.val(value).change(); + } +}; + +Template.admin.onCreated(function() { + if (RocketChat.settings.cachedCollectionPrivate == null) { + RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ + name: 'private-settings', + eventType: 'onLogged' + }); + RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection; + RocketChat.settings.cachedCollectionPrivate.init(); + } + this.selectedRooms = new ReactiveVar({}); + return RocketChat.settings.collectionPrivate.find().observe({ + added: (data) => { + const selectedRooms = this.selectedRooms.get(); + if (data.type === 'roomPick') { + selectedRooms[data._id] = data.value; + this.selectedRooms.set(selectedRooms); + } + return TempSettings.insert(data); + }, + changed: (data) => { + const selectedRooms = this.selectedRooms.get(); + if (data.type === 'roomPick') { + selectedRooms[data._id] = data.value; + this.selectedRooms.set(selectedRooms); + } + return TempSettings.update(data._id, data); + }, + removed: (data) => { + const selectedRooms = this.selectedRooms.get(); + if (data.type === 'roomPick') { + delete selectedRooms[data._id]; + this.selectedRooms.set(selectedRooms); + } + return TempSettings.remove(data._id); + } + }); +}); + +Template.admin.onDestroyed(function() { + return TempSettings.remove({}); +}); + +Template.admin.helpers({ + languages() { + const languages = TAPi18n.getLanguages(); + let result = []; + Object.keys(languages).forEach((key) => { + const language = languages[key]; + result.push(_.extend(language, { + key + })); + + }); + result = _.sortBy(result, 'key'); + result.unshift({ + 'name': 'Default', + 'en': 'Default', + 'key': '' + }); + return result; + }, + appLanguage(key) { + const setting = RocketChat.settings.get('Language'); + return setting && setting.split('-').shift().toLowerCase() === key; + }, + group() { + const groupId = FlowRouter.getParam('group'); + const group = RocketChat.settings.collectionPrivate.findOne({ + _id: groupId, + type: 'group' + }); + if (!group) { + return; + } + const settings = RocketChat.settings.collectionPrivate.find({ + group: groupId + }, { + sort: { + section: 1, + sorter: 1, + i18nLabel: 1 + } + }).fetch(); + const sections = {}; + Object.keys(settings).forEach((key) => { + const setting = settings[key]; + if (setting.i18nDefaultQuery != null) { + if (_.isString(setting.i18nDefaultQuery)) { + i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery); + } else { + i18nDefaultQuery = setting.i18nDefaultQuery; + } + if (!_.isArray(i18nDefaultQuery)) { + i18nDefaultQuery = [i18nDefaultQuery]; + } + Object.keys(i18nDefaultQuery).forEach((key) => { + const item = i18nDefaultQuery[key]; + if (RocketChat.settings.collectionPrivate.findOne(item) != null) { + setting.value = TAPi18n.__(`${ setting._id }_Default`); + } + }); + } + if (sections[setting.section || ''] == null) { + sections[setting.section] = []; + } + sections[setting.section || ''].push(setting); + }); + + Object.keys(settings).forEach((key) =>{ + const setting = settings[key]; + if (setting.i18nDefaultQuery != null) { + if (_.isString(setting.i18nDefaultQuery)) { + i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery); + } else { + i18nDefaultQuery = setting.i18nDefaultQuery; + } + if (!_.isArray(i18nDefaultQuery)) { + i18nDefaultQuery = [i18nDefaultQuery]; + } + Object.keys(i18nDefaultQuery).forEach((key) => { + const item = i18nDefaultQuery[key]; + if (RocketChat.settings.collectionPrivate.findOne(item) != null) { + setting.value = TAPi18n.__(`${ setting._id }_Default`); + } + }); + + } + if (sections[setting.section || ''] == null) { + sections[setting.section] = []; + } + sections[setting.section || ''].push(setting); + }); + group.sections = []; + Object.keys(sections).forEach((key) =>{ + const value = sections[key]; + group.sections.push({ + section: key, + settings: value + }); + }); + return group; + }, + i18nDefaultValue() { + return TAPi18n.__(`${ this._id }_Default`); + }, + isDisabled() { + let enableQuery; + if (this.blocked) { + return { + disabled: 'disabled' + }; + } + if (this.enableQuery == null) { + return {}; + } + if (_.isString(this.enableQuery)) { + enableQuery = JSON.parse(this.enableQuery); + } else { + enableQuery = this.enableQuery; + } + if (!_.isArray(enableQuery)) { + enableQuery = [enableQuery]; + } + let found = 0; + + Object.keys(enableQuery).forEach((key) =>{ + const item = enableQuery[key]; + if (TempSettings.findOne(item) != null) { + found++; + } + }); + if (found === enableQuery.length) { + return {}; + } else { + return { + disabled: 'disabled' + }; + } + }, + isReadonly() { + if (this.readonly === true) { + return { + readonly: 'readonly' + }; + } + }, + hasChanges(section) { + const group = FlowRouter.getParam('group'); + const query = { + group, + changed: true + }; + if (section != null) { + if (section === '') { + query.$or = [ + { + section: '' + }, { + section: { + $exists: false + } + } + ]; + } else { + query.section = section; + } + } + return TempSettings.find(query).count() > 0; + }, + isSettingChanged(id) { + return TempSettings.findOne({ + _id: id + }, { + fields: { + changed: 1 + } + }).changed; + }, + translateSection(section) { + if (section.indexOf(':') > -1) { + return section; + } + return t(section); + }, + label() { + const label = this.i18nLabel || this._id; + if (label) { + return TAPi18n.__(label); + } + }, + description() { + let description; + if (this.i18nDescription) { + description = TAPi18n.__(this.i18nDescription); + } + if ((description != null) && description !== this.i18nDescription) { + return description; + } + }, + sectionIsCustomOAuth(section) { + return /^Custom OAuth:\s.+/.test(section); + }, + callbackURL(section) { + const id = s.strRight(section, 'Custom OAuth: ').toLowerCase(); + return Meteor.absoluteUrl(`_oauth/${ id }`); + }, + relativeUrl(url) { + return Meteor.absoluteUrl(url); + }, + selectedOption(_id, val) { + const option = RocketChat.settings.collectionPrivate.findOne({ _id }); + return option && option.value === val; + }, + random() { + return Random.id(); + }, + getEditorOptions(readOnly) { + if (readOnly == null) { + readOnly = false; + } + return { + lineNumbers: true, + mode: this.code || 'javascript', + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + foldGutter: true, + matchBrackets: true, + autoCloseBrackets: true, + matchTags: true, + showTrailingSpace: true, + highlightSelectionMatches: true, + readOnly + }; + }, + setEditorOnBlur(_id) { + Meteor.defer(function() { + if (!$(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0]) { + return; + } + const codeMirror = $(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0].CodeMirror; + if (codeMirror.changeAdded === true) { + return; + } + const onChange = function() { + const value = codeMirror.getValue(); + return TempSettings.update({ + _id + }, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(_id).value !== value + } + }); + }; + const onChangeDelayed = _.debounce(onChange, 500); + codeMirror.on('change', onChangeDelayed); + return codeMirror.changeAdded = true; + }); + }, + assetAccept(fileConstraints) { + if (fileConstraints.extensions && fileConstraints.extensions.length) { + return `.${ fileConstraints.extensions.join(', .') }`; + } + }, + autocompleteRoom() { + return { + limit: 10, + rules: [ + { + collection: 'CachedChannelList', + subscription: 'channelAndPrivateAutocomplete', + field: 'name', + template: Template.roomSearch, + noMatchTemplate: Template.roomSearchEmpty, + matchAll: true, + selector(match) { + return { + name: match + }; + }, + sort: 'name' + } + ] + }; + }, + selectedRooms() { + return Template.instance().selectedRooms.get()[this._id] || []; + }, + getColorVariable(color) { + return color.replace(/theme-color-/, '@'); + }, + showResetButton() { + const setting = TempSettings.findOne({ + _id: this._id + }, { + fields: { + value: 1, + packageValue: 1 + } + }); + return this.type !== 'asset' && setting.value !== setting.packageValue && !this.blocked; + } +}); + +Template.admin.events({ + 'change .input-monitor, keyup .input-monitor': _.throttle(function(e) { + let value; + value = _.trim($(e.target).val()); + switch (this.type) { + case 'int': + value = parseInt(value); + break; + case 'boolean': + value = value === '1'; + } + return TempSettings.update({ + _id: this._id + }, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(this._id).value !== value + } + }); + }, 500), + 'change select[name=color-editor]'(e) { + const value = _.trim($(e.target).val()); + return TempSettings.update({ + _id: this._id + }, { + $set: { + editor: value + } + }); + }, + 'click .submit .discard'() { + const group = FlowRouter.getParam('group'); + const query = { + group, + changed: true + }; + const settings = TempSettings.find(query, { + fields: { + _id: 1, + value: 1, + packageValue: 1 + } + }).fetch(); + return settings.forEach(function(setting) { + const oldSetting = RocketChat.settings.collectionPrivate.findOne({ + _id: setting._id + }, { + fields: { + value: 1, + type: 1, + editor: 1 + } + }); + return setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor); + }); + }, + 'click .reset-setting'(e) { + e.preventDefault(); + let settingId = $(e.target).data('setting'); + if (typeof settingId === 'undefined') { + settingId = $(e.target).parent().data('setting'); + } + const defaultValue = getDefaultSetting(settingId); + return setFieldValue(settingId, defaultValue.packageValue, defaultValue.type, defaultValue.editor); + }, + 'click .reset-group'(e) { + let settings; + e.preventDefault(); + const group = FlowRouter.getParam('group'); + const section = $(e.target).data('section'); + if (section === '') { + settings = TempSettings.find({ + group, + section: { + $exists: false + } + }, { + fields: { + _id: 1 + } + }).fetch(); + } else { + settings = TempSettings.find({ + group, + section + }, { + fields: { + _id: 1 + } + }).fetch(); + } + return settings.forEach(function(setting) { + const defaultValue = getDefaultSetting(setting._id); + setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor); + return TempSettings.update({ + _id: setting._id + }, { + $set: { + value: defaultValue.packageValue, + changed: RocketChat.settings.collectionPrivate.findOne(setting._id).value !== defaultValue.packageValue + } + }); + }); + }, + 'click .submit .save'() { + const group = FlowRouter.getParam('group'); + const query = { + group, + changed: true + }; + const settings = TempSettings.find(query, { + fields: { + _id: 1, + value: 1, + editor: 1 + } + }).fetch(); + if (!_.isEmpty(settings)) { + return RocketChat.settings.batchSet(settings, function(err) { + if (err) { + return handleError(err); + } + TempSettings.update({ + changed: true + }, { + $unset: { + changed: 1 + } + }); + return toastr.success(TAPi18n.__('Settings_updated')); + }); + } + }, + 'click .submit .refresh-clients'() { + return Meteor.call('refreshClients', function() { + return toastr.success(TAPi18n.__('Clients_will_refresh_in_a_few_seconds')); + }); + }, + 'click .submit .add-custom-oauth'() { + const config = { + title: TAPi18n.__('Add_custom_oauth'), + text: TAPi18n.__('Give_a_unique_name_for_the_custom_oauth'), + type: 'input', + showCancelButton: true, + closeOnConfirm: true, + inputPlaceholder: TAPi18n.__('Custom_oauth_unique_name') + }; + return swal(config, function(inputValue) { + if (inputValue === false) { + return false; + } + if (inputValue === '') { + swal.showInputError(TAPi18n.__('Name_cant_be_empty')); + return false; + } + return Meteor.call('addOAuthService', inputValue, function(err) { + if (err) { + return handleError(err); + } + }); + }); + }, + 'click .submit .refresh-oauth'() { + toastr.info(TAPi18n.__('Refreshing')); + return Meteor.call('refreshOAuthService', function(err) { + if (err) { + return handleError(err); + } else { + return toastr.success(TAPi18n.__('Done')); + } + }); + }, + 'click .submit .remove-custom-oauth'() { + const name = this.section.replace('Custom OAuth: ', ''); + const config = { + title: TAPi18n.__('Are_you_sure'), + type: 'input', + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: TAPi18n.__('Yes_delete_it'), + cancelButtonText: TAPi18n.__('Cancel'), + closeOnConfirm: true + }; + return swal(config, function() { + return Meteor.call('removeOAuthService', name); + }); + }, + 'click .delete-asset'() { + return Meteor.call('unsetAsset', this.asset); + }, + 'change input[type=file]'(ev) { + const e = ev.originalEvent || ev; + let files = e.target.files; + if (!files || files.length === 0) { + if (e.dataTransfer && e.dataTransfer.files) { + files = e.dataTransfer.files; + } else { + files = []; + } + } + const results = []; + for (i = 0, len = files.length; i < len; i++) { + blob = files[i]; + toastr.info(TAPi18n.__('Uploading_file')); + reader = new FileReader(); + reader.readAsBinaryString(blob); + results.push(reader.onloadend = (function(_this) { + return function() { + return Meteor.call('setAsset', reader.result, blob.type, _this.asset, function(err, data) { + if (err != null) { + handleError(err); + console.log(err); + return; + } + return toastr.success(TAPi18n.__('File_uploaded')); + }); + }; + }(this))); + } + return results; + }, + 'click .expand'(e) { + $(e.currentTarget).closest('.section').removeClass('section-collapsed'); + $(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse')); + return $('.CodeMirror').each(function(index, codeMirror) { + return codeMirror.CodeMirror.refresh(); + }); + }, + 'click .collapse'(e) { + $(e.currentTarget).closest('.section').addClass('section-collapsed'); + return $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); + }, + 'click button.action'(e) { + if (this.type !== 'action') { + return; + } + return Meteor.call(this.value, function(err, data) { + let args; + if (err != null) { + err.details = _.extend(err.details || {}, { + errorTitle: 'Error' + }); + handleError(err); + return; + } + args = [data.message].concat(data.params); + return toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success')); + }); + }, + 'click .button-fullscreen'() { + let codeMirrorBox; + codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); + codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color'); + return codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); + }, + 'click .button-restore'() { + let codeMirrorBox; + codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); + codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color'); + return codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); + }, + 'autocompleteselect .autocomplete'(event, instance, doc) { + let selectedRooms, value; + selectedRooms = instance.selectedRooms.get(); + selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc); + instance.selectedRooms.set(selectedRooms); + value = selectedRooms[this.id]; + TempSettings.update({ + _id: this.id + }, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(this.id).value !== value + } + }); + event.currentTarget.value = ''; + return event.currentTarget.focus(); + }, + 'click .remove-room'(event, instance) { + let docId, selectedRooms, settingId, value; + docId = this._id; + settingId = event.currentTarget.getAttribute('data-setting'); + selectedRooms = instance.selectedRooms.get(); + selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], function(setting) { + return setting._id === docId; + }); + instance.selectedRooms.set(selectedRooms); + value = selectedRooms[settingId]; + return TempSettings.update({ + _id: settingId + }, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(settingId).value !== value + } + }); + } +}); + +Template.admin.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + return SideNav.openFlex(); + }); + return Tracker.autorun(function() { + let hasColor; + hasColor = TempSettings.findOne({ + group: FlowRouter.getParam('group'), + type: 'color' + }, { + fields: { + _id: 1 + } + }); + if (hasColor) { + return Meteor.setTimeout(function() { + return $('.colorpicker-input').each(function(index, el) { + return new jscolor(el); + }); + }, 400); + } + }); +}); -- GitLab From b7fc7b68daa0b0c9bc81778f5aca60165a5c389d Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 28 Apr 2017 17:46:05 -0300 Subject: [PATCH 025/280] Use new collection for avatars only --- lib/fileUpload.js | 10 +- .../client/lib/FileUploadAmazonS3.js | 11 +- .../client/lib/FileUploadGoogleStorage.js | 11 +- .../server/config/configFileUploadAmazonS3.js | 3 +- .../config/configFileUploadGoogleStorage.js | 3 +- .../rocketchat-lib/client/models/Avatars.js | 6 + packages/rocketchat-lib/package.js | 2 + .../rocketchat-lib/server/models/Avatars.js | 105 ++++++++++++++++++ .../server/models/Uploads.coffee | 51 --------- server/methods/setAvatarFromService.js | 9 +- server/startup/avatar.js | 2 +- 11 files changed, 150 insertions(+), 63 deletions(-) create mode 100644 packages/rocketchat-lib/client/models/Avatars.js create mode 100644 packages/rocketchat-lib/server/models/Avatars.js diff --git a/lib/fileUpload.js b/lib/fileUpload.js index f466da9ceb..93ae939792 100644 --- a/lib/fileUpload.js +++ b/lib/fileUpload.js @@ -74,9 +74,9 @@ if (UploadFS) { }); Meteor.fileStoreAvatar = new UploadFS.store.GridFS({ - collection: RocketChat.models.Uploads.model, + collection: RocketChat.models.Avatars.model, name: 'rocketchat_uploads_avatar', - collectionName: 'rocketchat_uploads', + collectionName: 'rocketchat_avatars', // filter: new UploadFS.Filter({ // onCheck: FileUpload.validateFileUpload // }), @@ -110,12 +110,12 @@ if (UploadFS) { onFinishUpload(file) { // update file record to match user's username const user = RocketChat.models.Users.findOneById(file.userId); - const oldAvatar = RocketChat.models.Uploads.findOneByName(`${ user.username }.avatar`); + const oldAvatar = RocketChat.models.Avatars.findOneByName(`${ user.username }.avatar`); if (oldAvatar) { Meteor.fileStoreAvatar.delete(oldAvatar._id); - RocketChat.models.Uploads.deleteFile(oldAvatar._id); + RocketChat.models.Avatars.deleteFile(oldAvatar._id); } - RocketChat.models.Uploads.updateFileNameById(file._id, `${ user.username }.avatar`); + RocketChat.models.Avatars.updateFileNameById(file._id, `${ user.username }.avatar`); // console.log('upload finished ->', file); }, diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js index df9fa58e3f..7d8250b7dd 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js @@ -3,7 +3,12 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { constructor(directive, meta, file) { super(meta, file); - this.uploader = new Slingshot.Upload(directive, meta); + this.directive = directive; + const directives = { + 'upload': 'rocketchat-uploads', + 'avatar': 'rocketchat-avatars' + }; + this.uploader = new Slingshot.Upload(directives[directive], meta); } start(callback) { @@ -19,6 +24,10 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; + if (this.directive === 'avatar') { + return callback(null, file); + } + Meteor.call('sendFileMessage', this.meta.rid, 's3', file, () => { Meteor.setTimeout(() => { callback.call(this, null, file); diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js index b1db2b767d..1ac7279e4b 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js @@ -3,7 +3,12 @@ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileUploadBase { constructor(directive, meta, file) { super(meta, file); - this.uploader = new Slingshot.Upload(directive, { rid: meta.rid }); + this.directive = directive; + const directives = { + 'upload': 'rocketchat-uploads-gs', + 'avatar': 'rocketchat-avatars-gs' + }; + this.uploader = new Slingshot.Upload(directives[directive], { rid: meta.rid }); } start(callback) { @@ -19,6 +24,10 @@ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileU file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; + if (this.directive === 'avatar') { + return callback(null, file); + } + Meteor.call('sendFileMessage', this.meta.rid, 'googleCloudStorage', file, () => { Meteor.setTimeout(() => { callback.call(this, null, file); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index 95d1dcd843..a1844ba22e 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -105,7 +105,8 @@ const configureSlingshot = _.debounce(() => { path } }; - RocketChat.models.Uploads.insertAvatarFileInit(user.username, this.userId, 's3', file, upload); + delete file.name; + RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 's3', file, upload); return path + user.username; } diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js index b2d1c6b9a8..e7a2b7b4fc 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js @@ -121,7 +121,8 @@ const createGoogleStorageDirective = _.debounce(() => { path } }; - RocketChat.models.Uploads.insertAvatarFileInit(user.username, this.userId, 'googleCloudStorage', file, upload); + delete file.name; + RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 'googleCloudStorage', file, upload); return path + user.username; } diff --git a/packages/rocketchat-lib/client/models/Avatars.js b/packages/rocketchat-lib/client/models/Avatars.js new file mode 100644 index 0000000000..bd8804e746 --- /dev/null +++ b/packages/rocketchat-lib/client/models/Avatars.js @@ -0,0 +1,6 @@ +RocketChat.models.Avatars = new class extends RocketChat.models._Base { + constructor() { + super(); + this._initModel('avatars'); + } +}; diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 8902fce8f2..9ec626890d 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -104,6 +104,7 @@ Package.onUse(function(api) { // SERVER MODELS api.addFiles('server/models/_Base.js', 'server'); + api.addFiles('server/models/Avatars.js', 'server'); api.addFiles('server/models/Messages.coffee', 'server'); api.addFiles('server/models/Reports.js', 'server'); api.addFiles('server/models/Rooms.coffee', 'server'); @@ -195,6 +196,7 @@ Package.onUse(function(api) { // CLIENT MODELS api.addFiles('client/models/_Base.coffee', 'client'); + api.addFiles('client/models/Avatars.js', 'client'); api.addFiles('client/models/Uploads.coffee', 'client'); // CLIENT VIEWS diff --git a/packages/rocketchat-lib/server/models/Avatars.js b/packages/rocketchat-lib/server/models/Avatars.js new file mode 100644 index 0000000000..43c7b6be6a --- /dev/null +++ b/packages/rocketchat-lib/server/models/Avatars.js @@ -0,0 +1,105 @@ +RocketChat.models.Avatars = new class extends RocketChat.models._Base { + constructor() { + super('avatars'); + + this.tryEnsureIndex({ name: 1 }); + } + + insertAvatarFileInit(name, userId, store, file, extra) { + const fileData = { + _id: name, + name, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + return this.insertOrUpsert(fileData); + } + + updateFileComplete(fileId, userId, file) { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + findOneByName(name) { + return this.findOne({ name }); + } + + updateFileNameById(fileId, name) { + const filter = { _id: fileId }; + const update = { + $set: { + name + } + }; + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + // @TODO deprecated + updateFileCompleteByNameAndUserId(name, userId, url) { + if (!name) { + return; + } + + const filter = { + name, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1, + url + } + }; + + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + deleteFile(fileId) { + if (this.model.direct && this.model.direct.remove) { + return this.model.direct.remove({ _id: fileId }); + } else { + return this.remove({ _id: fileId }); + } + } +}; diff --git a/packages/rocketchat-lib/server/models/Uploads.coffee b/packages/rocketchat-lib/server/models/Uploads.coffee index 6fc165a190..ff5edf5fd8 100644 --- a/packages/rocketchat-lib/server/models/Uploads.coffee +++ b/packages/rocketchat-lib/server/models/Uploads.coffee @@ -48,23 +48,6 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return file - insertAvatarFileInit: (name, userId, store, file, extra) -> - fileData = - name: "#{name}.avatar" - userId: userId - store: store - complete: false - uploading: true - progress: 0 - extension: s.strRightBack(file.name, '.') - uploadedAt: new Date() - - _.extend(fileData, file, extra); - - file = @insertOrUpsert fileData - - return file - updateFileComplete: (fileId, userId, file) -> if not fileId return @@ -88,42 +71,8 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base return result - # @TODO deprecated - updateFileCompleteByNameAndUserId: (name, userId, url) -> - if not name - return - - filter = - name: name - userId: userId - - update = - $set: - complete: true - uploading: false - progress: 1 - url: url - - if @model.direct?.update? - result = @model.direct.update filter, update - else - result = @update filter, update - - return result - - findOneByName: (name) -> - return @findOne name: name - deleteFile: (fileId) -> if @model.direct?.remove? return @model.direct.remove { _id: fileId } else return @remove { _id: fileId } - - updateFileNameById: (fileId, name) -> - filter = _id: fileId - update = $set: name: name - if @model.direct?.update? - return @model.direct.update filter, update - else - return @update filter, update diff --git a/server/methods/setAvatarFromService.js b/server/methods/setAvatarFromService.js index 8781e1c450..6f974d1a35 100644 --- a/server/methods/setAvatarFromService.js +++ b/server/methods/setAvatarFromService.js @@ -39,7 +39,11 @@ Meteor.methods({ } const user = RocketChat.models.Users.findOneById(Meteor.userId()); - const fileSaved = RocketChat.models.Uploads.findOneById(file._id); + const fileSaved = RocketChat.models.Avatars.findOneById(file._id); + + if (!fileSaved) { + return; + } if (fileSaved.userId !== user._id) { // this file is not user's avatar @@ -51,7 +55,8 @@ Meteor.methods({ return true; } - RocketChat.models.Uploads.updateFileCompleteByNameAndUserId(`${ user.username }.avatar`, Meteor.userId(), file.url); + RocketChat.models.Avatars.updateFileCompleteByNameAndUserId(user.username, user._id, file.url); + return true; } }); diff --git a/server/startup/avatar.js b/server/startup/avatar.js index cf31fcc236..ae69381aff 100644 --- a/server/startup/avatar.js +++ b/server/startup/avatar.js @@ -62,7 +62,7 @@ Meteor.startup(function() { return; } } - file = RocketChat.models.Uploads.findOneByName(`${ username }.avatar`); + file = RocketChat.models.Avatars.findOneByName(username); } if (file) { -- GitLab From 6ef9c390339d83de97e14819df2bfc926d79bea0 Mon Sep 17 00:00:00 2001 From: Matthew Shirley Date: Sat, 29 Apr 2017 09:24:17 -0700 Subject: [PATCH 026/280] Fixed incorrect error that user is not in room Fixed incorrect error that user is not in the room when the user is in the room. --- packages/rocketchat-slashcommands-mute/server/unmute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-slashcommands-mute/server/unmute.js b/packages/rocketchat-slashcommands-mute/server/unmute.js index c5a3f02cfa..0d5d6a93e1 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, -- GitLab From 76b34422cd57e78253ee05a47e8967b2b2a81e65 Mon Sep 17 00:00:00 2001 From: Matthew Shirley Date: Sat, 29 Apr 2017 09:28:07 -0700 Subject: [PATCH 027/280] Fixed incorrect error that user is not in the room Fixed incorrect error that user is not in the room when the user is in the room. --- packages/rocketchat-slashcommands-mute/server/mute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-slashcommands-mute/server/mute.js b/packages/rocketchat-slashcommands-mute/server/mute.js index 38273ff18f..21601cebe0 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, -- GitLab From ce812e804b46a290836eba9777b3f0bd42804fb6 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Mon, 1 May 2017 12:17:15 -0300 Subject: [PATCH 028/280] Delete converted file --- .../client/lib/fileUpload.coffee | 187 ------------------ 1 file changed, 187 deletions(-) delete mode 100644 packages/rocketchat-ui/client/lib/fileUpload.coffee diff --git a/packages/rocketchat-ui/client/lib/fileUpload.coffee b/packages/rocketchat-ui/client/lib/fileUpload.coffee deleted file mode 100644 index 55507fe061..0000000000 --- a/packages/rocketchat-ui/client/lib/fileUpload.coffee +++ /dev/null @@ -1,187 +0,0 @@ -readAsDataURL = (file, callback) -> - reader = new FileReader() - reader.onload = (ev) -> - callback ev.target.result, file - - reader.readAsDataURL file - -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 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] - - readAsDataURL file.file, (content) -> - callback(file, content) - else - callback(file, null) - -formatBytes = (bytes, decimals) -> - if bytes == 0 - return '0 Bytes' - - k = 1000 - dm = decimals + 1 or 3 - - sizes = [ - 'Bytes' - 'KB' - 'MB' - 'GB' - 'TB' - 'PB' - ] - - i = Math.floor(Math.log(bytes) / Math.log(k)) - - parseFloat((bytes / k ** i).toFixed(dm)) + ' ' + sizes[i] - -readAsArrayBuffer = (file, callback) -> - reader = new FileReader() - reader.onload = (ev) -> - callback ev.target.result, file - - reader.readAsArrayBuffer file - - -@fileUpload = (files) -> - roomId = Session.get('openedRoom') - files = [].concat files - - consume = -> - file = files.pop() - if not file? - swal.close() - return - - if not RocketChat.fileUploadIsValidContentType file.file.type - swal - title: t('FileUpload_MediaType_NotAccepted') - text: file.file.type || "*.#{s.strRightBack(file.file.name, '.')}" - type: 'error' - timer: 3000 - return - - if file.file.size is 0 - swal - title: t('FileUpload_File_Empty') - type: 'error' - timer: 1000 - return - - getUploadPreview file, (file, preview) -> - text = '' - - if file.type is 'audio' - text = """ -
- -
-
- - -
- """ - else if file.type is 'video' - text = """ -
- -
-
- - -
- """ - else if file.type is 'image' - text = """ -
-
-
-
- - -
- """ - else - fileSize = formatBytes(file.file.size) - - text = """ -
-
#{Handlebars._escape(file.name)} - #{fileSize}
-
-
- - -
- """ - - swal - title: t('Upload_file_question') - text: text - showCancelButton: true - closeOnConfirm: false - closeOnCancel: false - confirmButtonText: t('Send') - cancelButtonText: t('Cancel') - html: true - , (isConfirm) -> - consume() - if isConfirm isnt true - return - - record = - name: document.getElementById('file-name').value or file.name or file.file.name - size: file.file.size - type: file.file.type - rid: roomId - description: document.getElementById('file-description').value - - upload = fileUploadHandler record, file.file - - uploading = Session.get('uploading') or [] - uploading.push - id: upload.id - name: upload.getFileName() - percentage: 0 - - Session.set 'uploading', uploading - - upload.onProgress = (progress) -> - uploading = Session.get('uploading') - - item = _.findWhere(uploading, {id: upload.id}) - if item? - item.percentage = Math.round(progress * 100) or 0 - Session.set 'uploading', uploading - - upload.start() - - Tracker.autorun (c) -> - cancel = Session.get "uploading-cancel-#{upload.id}" - if cancel - upload.stop() - c.stop() - - uploading = Session.get 'uploading' - if uploading? - item = _.findWhere(uploading, {id: upload.id}) - if item? - item.percentage = 0 - Session.set 'uploading', uploading - - Meteor.setTimeout -> - uploading = Session.get 'uploading' - if uploading? - item = _.findWhere(uploading, {id: upload.id}) - Session.set 'uploading', _.without(uploading, item) - , 1000 - - consume() -- GitLab From 1c72057feb6d2b02c3b2b02de99a56c3a6e992c7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Mon, 1 May 2017 12:18:05 -0300 Subject: [PATCH 029/280] Avatar: Add support to FileSystem and various fixes --- lib/fileUpload.js | 31 +-- .../client/lib/FileUploadAmazonS3.js | 11 +- .../client/lib/FileUploadFileSystem.js | 50 ++--- .../client/lib/FileUploadGoogleStorage.js | 11 +- .../client/lib/FileUploadGridFS.js | 10 +- .../config/configFileUploadFileSystem.js | 125 +++++++++--- .../server/lib/FileUpload.js | 10 + .../.npm/package/npm-shrinkwrap.json | 188 +++--------------- .../rocketchat-ui/client/lib/fileUpload.js | 27 ++- 9 files changed, 175 insertions(+), 288 deletions(-) diff --git a/lib/fileUpload.js b/lib/fileUpload.js index 93ae939792..782015a73c 100644 --- a/lib/fileUpload.js +++ b/lib/fileUpload.js @@ -80,42 +80,17 @@ if (UploadFS) { // filter: new UploadFS.Filter({ // onCheck: FileUpload.validateFileUpload // }), - transformWrite(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { - return readStream.pipe(writeStream); - } - - let stream = undefined; - - const identify = function(err, data) { - if (err) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; - - if (data.Orientation && !['', 'Unknown', 'Undefined'].includes(data.Orientation)) { - RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - stream.pipe(writeStream); - } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - }, + transformWrite: FileUpload.avatarTransformWrite, onFinishUpload(file) { // update file record to match user's username const user = RocketChat.models.Users.findOneById(file.userId); - const oldAvatar = RocketChat.models.Avatars.findOneByName(`${ user.username }.avatar`); + const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username); if (oldAvatar) { Meteor.fileStoreAvatar.delete(oldAvatar._id); RocketChat.models.Avatars.deleteFile(oldAvatar._id); } - RocketChat.models.Avatars.updateFileNameById(file._id, `${ user.username }.avatar`); + RocketChat.models.Avatars.updateFileNameById(file._id, user.username); // console.log('upload finished ->', file); }, diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js index 7d8250b7dd..0c6f3efd90 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js @@ -3,7 +3,6 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { constructor(directive, meta, file) { super(meta, file); - this.directive = directive; const directives = { 'upload': 'rocketchat-uploads', 'avatar': 'rocketchat-avatars' @@ -24,15 +23,7 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; - if (this.directive === 'avatar') { - return callback(null, file); - } - - Meteor.call('sendFileMessage', this.meta.rid, 's3', file, () => { - Meteor.setTimeout(() => { - callback.call(this, null, file); - }, 2000); - }); + return callback(null, file, 's3'); } }); diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js b/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js index d19c5c0314..025a3cf781 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js @@ -1,4 +1,4 @@ -/* globals FileUploadBase, UploadFS, FileUpload:true, FileSystemStore:true */ +/* globals FileUploadBase, UploadFS, FileUpload:true, FileSystemStore:true, FileSystemStoreAvatar:true */ FileSystemStore = new UploadFS.store.Local({ collection: RocketChat.models.Uploads.model, @@ -8,51 +8,41 @@ FileSystemStore = new UploadFS.store.Local({ }) }); +FileSystemStoreAvatar = new UploadFS.store.Local({ + collection: RocketChat.models.Avatars.model, + name: 'fileSystemAvatar', + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }) +}); + FileUpload.FileSystem = class FileUploadFileSystem extends FileUploadBase { - constructor(meta, file) { + constructor(directive, meta, file) { super(meta, file); + console.log('filesystem', {directive, meta, file}); + this.store = directive === 'avatar' ? FileSystemStoreAvatar : FileSystemStore; + } + + start(callback) { this.handler = new UploadFS.Uploader({ - store: FileSystemStore, - data: file, - file: meta, + store: this.store, + data: this.file, + file: this.meta, onError: (err) => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - if (item != null) { - item.error = err.reason; - item.percentage = 0; - } - return Session.set('uploading', uploading); - } + return callback(err); }, onComplete: (fileData) => { const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); + return callback(null, file, 'fs'); } }); this.handler.onProgress = (file, progress) => { this.onProgress(progress); }; - } - start() { return this.handler.start(); } diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js index 1ac7279e4b..b4b047c42b 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js @@ -3,7 +3,6 @@ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileUploadBase { constructor(directive, meta, file) { super(meta, file); - this.directive = directive; const directives = { 'upload': 'rocketchat-uploads-gs', 'avatar': 'rocketchat-avatars-gs' @@ -24,15 +23,7 @@ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileU file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; - if (this.directive === 'avatar') { - return callback(null, file); - } - - Meteor.call('sendFileMessage', this.meta.rid, 'googleCloudStorage', file, () => { - Meteor.setTimeout(() => { - callback.call(this, null, file); - }, 2000); - }); + return callback(null, file, 'googleCloudStorage'); } }); diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js index d5236b1c97..6b8c3755b9 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js @@ -2,7 +2,6 @@ FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { constructor(directive, meta, file) { super(meta, file); - this.directive = directive; this.store = directive === 'avatar' ? Meteor.fileStoreAvatar : Meteor.fileStore; } @@ -16,15 +15,8 @@ FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { }, onComplete: (fileData) => { const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); - if (this.directive === 'avatar') { - return callback(null, file); - } file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { - Meteor.setTimeout(() => { - return callback(null, file); - }, 2000); - }); + return callback(null, file, 'gridfs'); } }); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js b/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js index f97534577a..0ba17089bf 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js @@ -1,47 +1,70 @@ -/* globals FileSystemStore:true, FileUpload, UploadFS, RocketChatFile */ +/* globals FileSystemStore:true, FileUpload, UploadFS, RocketChatFile, FileSystemStoreAvatar */ -const storeName = 'fileSystem'; +const transformWrite = function(readStream, writeStream, fileId, file) { + if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { + return readStream.pipe(writeStream); + } + + let stream = undefined; + + const identify = function(err, data) { + if (err != null) { + return stream.pipe(writeStream); + } + + file.identify = { + format: data.format, + size: data.size + }; + + if ([null, undefined, '', 'Unknown', 'Undefined'].indexOf(data.Orientation) === -1) { + return RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); + } else { + return stream.pipe(writeStream); + } + }; + + stream = RocketChatFile.gm(readStream).identify(identify).stream(); + return; +}; FileSystemStore = null; +FileSystemStoreAvatar = null; const createFileSystemStore = _.debounce(function() { const stores = UploadFS.getStores(); - if (stores[storeName]) { - delete stores[storeName]; - } + delete stores.fileSystem; + delete stores.fileSystemAvatar; + FileSystemStore = new UploadFS.store.Local({ - collection: RocketChat.models.Uploads.model, - name: storeName, path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', + collection: RocketChat.models.Uploads.model, filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload }), - transformWrite(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { - return readStream.pipe(writeStream); - } - - let stream = undefined; - - const identify = function(err, data) { - if (err != null) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; + name: 'fileSystem', + transformWrite + }); - if ([null, undefined, '', 'Unknown', 'Undefined'].indexOf(data.Orientation) === -1) { - return RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - return stream.pipe(writeStream); + FileSystemStoreAvatar = new UploadFS.store.Local({ + path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', + collection: RocketChat.models.Avatars.model, + name: 'fileSystemAvatar', + transformWrite: FileUpload.avatarTransformWrite, + onFinishUpload(file) { + // update file record to match user's username + const user = RocketChat.models.Users.findOneById(file.userId); + const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username); + if (oldAvatar) { + try { + FileSystemStoreAvatar.delete(oldAvatar._id); + RocketChat.models.Avatars.deleteFile(oldAvatar._id); + } catch (e) { + console.error(e); } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - return; + } + RocketChat.models.Avatars.updateFileNameById(file._id, user.username); + // console.log('upload finished ->', file); } }); }, 500); @@ -50,7 +73,7 @@ RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); const fs = Npm.require('fs'); -FileUpload.addHandler(storeName, { +FileUpload.addHandler('fileSystem', { get(file, req, res) { const filePath = FileSystemStore.getFilePath(file._id, file); @@ -77,3 +100,41 @@ FileUpload.addHandler(storeName, { return FileSystemStore.delete(file._id); } }); + +FileUpload.addHandler('fileSystemAvatar', { + get(file, req, res) { + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + const filePath = FileSystemStoreAvatar.getFilePath(file._id, file); + + try { + const stat = Meteor.wrapAsync(fs.stat)(filePath); + + if (stat && stat.isFile()) { + file = FileUpload.addExtensionTo(file); + res.setHeader('Content-Disposition', 'inline'); + res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); + res.setHeader('Content-Type', file.type); + res.setHeader('Content-Length', file.size); + + FileSystemStoreAvatar.getReadStream(file._id, file).pipe(res); + } + } catch (e) { + res.writeHead(404); + res.end(); + return; + } + }, + + delete(file) { + return FileSystemStoreAvatar.delete(file._id); + } +}); diff --git a/packages/rocketchat-file-upload/server/lib/FileUpload.js b/packages/rocketchat-file-upload/server/lib/FileUpload.js index 05ec504af3..7e207eef10 100644 --- a/packages/rocketchat-file-upload/server/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/server/lib/FileUpload.js @@ -41,3 +41,13 @@ FileUpload.addExtensionTo = function(file) { return file; }; + +FileUpload.avatarTransformWrite = function(readStream, writeStream, fileId, file) { + if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { + return readStream.pipe(writeStream); + } + const height = RocketChat.settings.get('Accounts_AvatarSize'); + const width = height; + return RocketChatFile.gm(readStream).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); +}; + 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 a346132590..c931564c62 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -1,8 +1,8 @@ { "dependencies": { "ajv": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "from": "ajv@>=4.9.1 <5.0.0" }, "ansi-regex": { @@ -30,11 +30,6 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "from": "arrify@>=1.0.1 <2.0.0" }, - "ascli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", - "from": "ascli@>=1.0.0 <2.0.0" - }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -46,8 +41,8 @@ "from": "assert-plus@>=0.2.0 <0.3.0" }, "async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.3.0.tgz", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz", "from": "async@>=2.1.2 <3.0.0" }, "asynckit": { @@ -65,11 +60,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "from": "aws4@>=1.2.1 <2.0.0" }, - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "from": "balanced-match@>=0.4.1 <0.5.0" - }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", @@ -85,11 +75,6 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "from": "boom@>=2.0.0 <3.0.0" }, - "brace-expansion": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "from": "brace-expansion@>=1.0.0 <2.0.0" - }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -100,16 +85,6 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "from": "buffer-shims@>=1.0.0 <1.1.0" }, - "bytebuffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", - "from": "bytebuffer@>=5.0.0 <6.0.0" - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "from": "camelcase@>=2.0.1 <3.0.0" - }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -125,26 +100,11 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "from": "chalk@>=1.1.1 <2.0.0" }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "from": "cliui@>=3.0.3 <4.0.0" - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "from": "co@>=4.6.0 <5.0.0" }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "from": "code-point-at@>=1.0.0 <2.0.0" - }, - "colour": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", - "from": "colour@>=0.7.1 <0.8.0" - }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -155,11 +115,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "from": "commander@>=2.9.0 <3.0.0" }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "from": "concat-map@0.0.1" - }, "concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", @@ -192,11 +147,6 @@ } } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "from": "decamelize@>=1.1.1 <2.0.0" - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -233,8 +183,8 @@ "from": "escape-string-regexp@>=1.0.2 <2.0.0" }, "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "from": "extend@>=3.0.0 <4.0.0" }, "extsprintf": { @@ -252,11 +202,6 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", "from": "form-data@>=2.1.1 <2.2.0" }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "from": "fs.realpath@>=1.0.0 <2.0.0" - }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -279,11 +224,6 @@ } } }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "from": "glob@>=7.0.5 <8.0.0" - }, "google-auth-library": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz", @@ -322,8 +262,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.2.4.tgz", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.0.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -378,8 +318,8 @@ "from": "npmlog@>=4.0.2 <5.0.0", "dependencies": { "are-we-there-yet": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "from": "are-we-there-yet@>=1.1.2 <1.2.0", "dependencies": { "delegates": { @@ -390,7 +330,7 @@ "readable-stream": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "from": "readable-stream@>=2.0.0 <3.0.0||>=1.1.13 <2.0.0", + "from": "readable-stream@>=2.0.6 <3.0.0", "dependencies": { "buffer-shims": { "version": "1.0.0", @@ -437,8 +377,8 @@ "from": "console-control-strings@>=1.1.0 <1.2.0" }, "gauge": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "from": "gauge@>=2.7.1 <2.8.0", "dependencies": { "aproba": { @@ -598,8 +538,8 @@ "from": "har-validator@>=4.2.1 <4.3.0", "dependencies": { "ajv": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.6.tgz", + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", "from": "ajv@>=4.9.1 <5.0.0", "dependencies": { "co": { @@ -693,8 +633,8 @@ } }, "sshpk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.11.0.tgz", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", "from": "sshpk@>=1.7.0 <2.0.0", "dependencies": { "asn1": { @@ -723,8 +663,8 @@ "from": "ecc-jsbn@>=0.1.1 <0.2.0" }, "getpass": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "from": "getpass@>=0.1.1 <0.2.0" }, "jodid25519": { @@ -939,14 +879,14 @@ "from": "tar-pack@>=3.4.0 <4.0.0", "dependencies": { "debug": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.3.tgz", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.5.tgz", "from": "debug@>=2.2.0 <3.0.0", "dependencies": { "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "from": "ms@0.7.2" + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.3.tgz", + "from": "ms@0.7.3" } } }, @@ -1006,7 +946,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.0.0 <2.0.0", + "from": "once@>=1.3.3 <2.0.0", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1103,31 +1043,16 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "from": "http-signature@>=1.1.0 <1.2.0" }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "from": "inflight@>=1.0.4 <2.0.0" - }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "from": "inherits@>=2.0.3 <3.0.0" }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "from": "invert-kv@>=1.0.0 <2.0.0" - }, "is": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", "from": "is@>=3.0.1 <4.0.0" }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0" - }, "is-my-json-valid": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", @@ -1215,11 +1140,6 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", "from": "jws@>=3.1.4 <4.0.0" }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "from": "lcid@>=1.0.0 <2.0.0" - }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -1238,7 +1158,7 @@ "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "from": "long@>=3.0.0 <4.0.0" + "from": "long@>=3.2.0 <4.0.0" }, "methmeth": { "version": "1.1.0", @@ -1260,11 +1180,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "from": "mime-types@>=2.1.7 <2.2.0" }, - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "from": "minimatch@>=3.0.2 <4.0.0" - }, "modelo": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz", @@ -1280,11 +1195,6 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "from": "node-forge@>=0.7.1 <0.8.0" }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "from": "number-is-nan@>=1.0.0 <2.0.0" - }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -1300,21 +1210,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "from": "once@>=1.3.0 <1.4.0" }, - "optjs": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", - "from": "optjs@>=3.2.2 <3.3.0" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "from": "os-locale@>=1.4.0 <2.0.0" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "from": "path-is-absolute@>=1.0.0 <2.0.0" - }, "performance-now": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", @@ -1341,9 +1236,9 @@ "from": "propprop@>=0.3.1 <0.4.0" }, "protobufjs": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", - "from": "protobufjs@>=5.0.0 <6.0.0" + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.7.3.tgz", + "from": "protobufjs@>=6.7.0 <7.0.0" }, "punycode": { "version": "1.4.1", @@ -1451,11 +1346,6 @@ "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz", "from": "string-format-obj@>=1.1.0 <2.0.0" }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "from": "string-width@>=1.0.1 <2.0.0" - }, "string_decoder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz", @@ -1521,16 +1411,6 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "from": "verror@1.3.6" }, - "window-size": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "from": "window-size@>=0.1.4 <0.2.0" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "from": "wrap-ansi@>=2.0.0 <3.0.0" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1540,16 +1420,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "from": "xtend@>=4.0.0 <5.0.0" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "from": "y18n@>=3.2.0 <4.0.0" - }, - "yargs": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "from": "yargs@>=3.10.0 <4.0.0" } } } diff --git a/packages/rocketchat-ui/client/lib/fileUpload.js b/packages/rocketchat-ui/client/lib/fileUpload.js index d7ef1590a4..20d2007951 100644 --- a/packages/rocketchat-ui/client/lib/fileUpload.js +++ b/packages/rocketchat-ui/client/lib/fileUpload.js @@ -145,7 +145,7 @@ fileUpload = function(filesToUpload) { description: document.getElementById('file-description').value }; - const upload = fileUploadHandler('rocketchat-uploads-gs', record, file.file); + const upload = fileUploadHandler('upload', record, file.file); let uploading = Session.get('uploading') || []; uploading.push({ @@ -166,7 +166,7 @@ fileUpload = function(filesToUpload) { } }; - upload.start(function(error, file) { + upload.start(function(error, file, storage) { if (error) { let uploading = Session.get('uploading'); if (!Array.isArray(uploading)) { @@ -187,14 +187,21 @@ fileUpload = function(filesToUpload) { Session.set('uploading', uploading); return; - } else if (file) { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } + } + + + if (file) { + Meteor.call('sendFileMessage', roomId, storage, file, () => { + Meteor.setTimeout(() => { + const uploading = Session.get('uploading'); + if (uploading !== null) { + const item = _.findWhere(uploading, { + id: this.id + }); + return Session.set('uploading', _.without(uploading, item)); + } + }, 2000); + }); } }); -- GitLab From 536fba74d7129cc4149f56cfb4408622b858f369 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Mon, 1 May 2017 18:31:09 -0300 Subject: [PATCH 030/280] Avatar: Refactor some code, allow set avatar from server for FS and GridFS --- lib/fileUpload.js | 4 +- .../client/lib/FileUploadFileSystem.js | 4 +- .../server/config/configFileUploadAmazonS3.js | 8 +- .../config/configFileUploadFileSystem.js | 168 +++++++++--------- .../config/configFileUploadGoogleStorage.js | 12 +- .../server/config/configFileUploadGridFS.js | 39 ++-- .../server/lib/FileUpload.js | 126 +++++++++---- .../server/functions/deleteMessage.js | 2 +- .../server/functions/deleteUser.js | 3 +- .../server/functions/setUserAvatar.js | 15 +- server/methods/deleteFileMessage.js | 2 +- server/methods/resetAvatar.js | 2 +- server/startup/initialData.js | 19 +- 13 files changed, 250 insertions(+), 154 deletions(-) diff --git a/lib/fileUpload.js b/lib/fileUpload.js index 782015a73c..f5eafb6e09 100644 --- a/lib/fileUpload.js +++ b/lib/fileUpload.js @@ -11,7 +11,7 @@ if (UploadFS) { Meteor.fileStore = new UploadFS.store.GridFS({ collection: RocketChat.models.Uploads.model, - name: 'rocketchat_uploads', + name: 'GridFS:Uploads', collectionName: 'rocketchat_uploads', filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload @@ -75,7 +75,7 @@ if (UploadFS) { Meteor.fileStoreAvatar = new UploadFS.store.GridFS({ collection: RocketChat.models.Avatars.model, - name: 'rocketchat_uploads_avatar', + name: 'GridFS:Avatars', collectionName: 'rocketchat_avatars', // filter: new UploadFS.Filter({ // onCheck: FileUpload.validateFileUpload diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js b/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js index 025a3cf781..10bb44cf74 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js @@ -2,7 +2,7 @@ FileSystemStore = new UploadFS.store.Local({ collection: RocketChat.models.Uploads.model, - name: 'fileSystem', + name: 'FileSystem:Uploads', filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload }) @@ -10,7 +10,7 @@ FileSystemStore = new UploadFS.store.Local({ FileSystemStoreAvatar = new UploadFS.store.Local({ collection: RocketChat.models.Avatars.model, - name: 'fileSystemAvatar', + name: 'FileSystem:Avatars', filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload }) diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index a1844ba22e..fe18ecaace 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -1,5 +1,6 @@ /* globals Slingshot, FileUpload, AWS */ -const crypto = Npm.require('crypto'); +import crypto from 'crypto'; +import { FileUploadClass } from '../lib/FileUpload'; let S3accessKey; let S3secretKey; @@ -16,7 +17,9 @@ const generateURL = function(file) { return `${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; }; -FileUpload.addHandler('s3', { +new FileUploadClass({ + name: 'S3:Uploads', + get(file, req, res) { const fileUrl = generateURL(file); @@ -26,6 +29,7 @@ FileUpload.addHandler('s3', { } res.end(); }, + delete(file) { const s3 = new AWS.S3(); const request = s3.deleteObject({ diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js b/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js index 0ba17089bf..9ffdb26a7a 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js @@ -1,81 +1,20 @@ -/* globals FileSystemStore:true, FileUpload, UploadFS, RocketChatFile, FileSystemStoreAvatar */ +/* globals FileUpload, UploadFS, RocketChatFile */ -const transformWrite = function(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { - return readStream.pipe(writeStream); - } +import fs from 'fs'; +import { FileUploadClass } from '../lib/FileUpload'; - let stream = undefined; - - const identify = function(err, data) { - if (err != null) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; +const insert = function(file, stream, cb) { + const fileId = this.store.create(file); - if ([null, undefined, '', 'Unknown', 'Undefined'].indexOf(data.Orientation) === -1) { - return RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - return stream.pipe(writeStream); - } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - return; + this.store.write(stream, fileId, cb); }; -FileSystemStore = null; -FileSystemStoreAvatar = null; +const FileSystemUploads = new FileUploadClass({ + name: 'FileSystem:Uploads', + // store setted bellow -const createFileSystemStore = _.debounce(function() { - const stores = UploadFS.getStores(); - delete stores.fileSystem; - delete stores.fileSystemAvatar; - - FileSystemStore = new UploadFS.store.Local({ - path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', - collection: RocketChat.models.Uploads.model, - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }), - name: 'fileSystem', - transformWrite - }); - - FileSystemStoreAvatar = new UploadFS.store.Local({ - path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', - collection: RocketChat.models.Avatars.model, - name: 'fileSystemAvatar', - transformWrite: FileUpload.avatarTransformWrite, - onFinishUpload(file) { - // update file record to match user's username - const user = RocketChat.models.Users.findOneById(file.userId); - const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username); - if (oldAvatar) { - try { - FileSystemStoreAvatar.delete(oldAvatar._id); - RocketChat.models.Avatars.deleteFile(oldAvatar._id); - } catch (e) { - console.error(e); - } - } - RocketChat.models.Avatars.updateFileNameById(file._id, user.username); - // console.log('upload finished ->', file); - } - }); -}, 500); - -RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); - -const fs = Npm.require('fs'); - -FileUpload.addHandler('fileSystem', { get(file, req, res) { - const filePath = FileSystemStore.getFilePath(file._id, file); + const filePath = this.store.getFilePath(file._id, file); try { const stat = Meteor.wrapAsync(fs.stat)(filePath); @@ -87,7 +26,7 @@ FileUpload.addHandler('fileSystem', { res.setHeader('Content-Type', file.type); res.setHeader('Content-Length', file.size); - FileSystemStore.getReadStream(file._id, file).pipe(res); + this.store.getReadStream(file._id, file).pipe(res); } } catch (e) { res.writeHead(404); @@ -96,12 +35,13 @@ FileUpload.addHandler('fileSystem', { } }, - delete(file) { - return FileSystemStore.delete(file._id); - } + insert }); -FileUpload.addHandler('fileSystemAvatar', { +const FileSystemAvatars = new FileUploadClass({ + name: 'FileSystem:Avatars', + // store setted bellow + get(file, req, res) { const reqModifiedHeader = req.headers['if-modified-since']; if (reqModifiedHeader) { @@ -113,7 +53,7 @@ FileUpload.addHandler('fileSystemAvatar', { } } - const filePath = FileSystemStoreAvatar.getFilePath(file._id, file); + const filePath = this.store.getFilePath(file._id, file); try { const stat = Meteor.wrapAsync(fs.stat)(filePath); @@ -125,7 +65,7 @@ FileUpload.addHandler('fileSystemAvatar', { res.setHeader('Content-Type', file.type); res.setHeader('Content-Length', file.size); - FileSystemStoreAvatar.getReadStream(file._id, file).pipe(res); + this.store.getReadStream(file._id, file).pipe(res); } } catch (e) { res.writeHead(404); @@ -134,7 +74,73 @@ FileUpload.addHandler('fileSystemAvatar', { } }, - delete(file) { - return FileSystemStoreAvatar.delete(file._id); - } + insert }); + + +const transformWrite = function(readStream, writeStream, fileId, file) { + if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { + return readStream.pipe(writeStream); + } + + let stream = undefined; + + const identify = function(err, data) { + if (err != null) { + return stream.pipe(writeStream); + } + + file.identify = { + format: data.format, + size: data.size + }; + + if ([null, undefined, '', 'Unknown', 'Undefined'].indexOf(data.Orientation) === -1) { + return RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); + } else { + return stream.pipe(writeStream); + } + }; + + stream = RocketChatFile.gm(readStream).identify(identify).stream(); + return; +}; + +const createFileSystemStore = _.debounce(function() { + const stores = UploadFS.getStores(); + delete stores['FileSystem:Uploads']; + delete stores['FileSystem:Avatars']; + + FileSystemUploads.store = new UploadFS.store.Local({ + path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', + collection: FileSystemUploads.model.model, + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }), + name: FileSystemUploads.name, + transformWrite + }); + + FileSystemAvatars.store = new UploadFS.store.Local({ + path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', + collection: FileSystemAvatars.model.model, + name: FileSystemAvatars.name, + transformWrite: FileUpload.avatarTransformWrite, + onFinishUpload(file) { + // update file record to match user's username + const user = RocketChat.models.Users.findOneById(file.userId); + const oldAvatar = FileSystemAvatars.model.findOneByName(user.username); + if (oldAvatar) { + try { + FileSystemAvatars.deleteById(oldAvatar._id); + } catch (e) { + console.error(e); + } + } + FileSystemAvatars.model.updateFileNameById(file._id, user.username); + // console.log('upload finished ->', file); + } + }); +}, 500); + +RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js index e7a2b7b4fc..69e0b514ba 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js @@ -1,6 +1,7 @@ -/* globals FileUpload, Slingshot, SystemLogger */ +/* globals FileUpload, Slingshot */ -const crypto = Npm.require('crypto'); +import crypto from 'crypto'; +import { FileUploadClass } from '../lib/FileUpload'; function generateUrlParts({ file }) { const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); @@ -62,7 +63,9 @@ function createDirective(directiveName, { key, bucket, accessId, secret }) { } } -FileUpload.addHandler('googleCloudStorage', { +new FileUploadClass({ + name: 'googleCloudStorage', + get(file, req, res) { const fileUrl = generateGetURL({ file }); @@ -72,12 +75,15 @@ FileUpload.addHandler('googleCloudStorage', { } res.end(); }, + delete(file) { if (!file || !file.googleCloudStorage) { console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, the file and googleCloudStorage properties are not defined.'); return; } + // RocketChat.models.Uploads.deleteFile(file._id); + const url = generateDeleteUrl({ file }); if (_.isEmpty(url)) { diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js b/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js index fad4015518..4e7553f642 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js @@ -1,7 +1,10 @@ /* globals FileUpload, UploadFS */ -const stream = Npm.require('stream'); -const zlib = Npm.require('zlib'); -const util = Npm.require('util'); +import stream from 'stream'; +import zlib from 'zlib'; +import util from 'util'; + +import { FileUploadClass } from '../lib/FileUpload'; + const logger = new Logger('FileUpload'); function ExtractRange(options) { @@ -124,7 +127,18 @@ const readFromGridFS = function(storeName, fileId, file, headers, req, res) { } }; -FileUpload.addHandler('rocketchat_uploads', { +const insert = function(file, stream, cb) { + const fileId = this.store.create(file); + + this.store.write(stream, fileId, cb); +}; + +new FileUploadClass({ + name: 'GridFS:Uploads', + getStore() { + return Meteor.fileStore; + }, + get(file, req, res) { file = FileUpload.addExtensionTo(file); const headers = { @@ -135,12 +149,16 @@ FileUpload.addHandler('rocketchat_uploads', { }; return readFromGridFS(file.store, file._id, file, headers, req, res); }, - delete(file) { - return Meteor.fileStore.delete(file._id); - } + + insert }); -FileUpload.addHandler('rocketchat_uploads_avatar', { +new FileUploadClass({ + name: 'GridFS:Avatars', + getStore() { + return Meteor.fileStoreAvatar + }, + get(file, req, res) { const reqModifiedHeader = req.headers['if-modified-since']; if (reqModifiedHeader) { @@ -163,7 +181,6 @@ FileUpload.addHandler('rocketchat_uploads_avatar', { }; return readFromGridFS(file.store, file._id, file, headers, req, res); }, - delete(file) { - return Meteor.fileStore.delete(file._id); - } + + insert }); diff --git a/packages/rocketchat-file-upload/server/lib/FileUpload.js b/packages/rocketchat-file-upload/server/lib/FileUpload.js index 7e207eef10..f606d0ad0d 100644 --- a/packages/rocketchat-file-upload/server/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/server/lib/FileUpload.js @@ -1,53 +1,109 @@ -/* globals FileUpload:true */ import mime from 'mime-type/with-db'; -FileUpload.handlers = {}; +Object.assign(FileUpload, { + handlers: {}, -FileUpload.addHandler = function(store, handler) { - this.handlers[store] = handler; -}; + avatarTransformWrite(readStream, writeStream/*, fileId, file*/) { + if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { + return readStream.pipe(writeStream); + } + const height = RocketChat.settings.get('Accounts_AvatarSize'); + const width = height; + return RocketChatFile.gm(readStream).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); + }, -FileUpload.delete = function(fileId) { - const file = RocketChat.models.Uploads.findOneById(fileId); + addExtensionTo(file) { + if (mime.lookup(file.name) === file.type) { + return file; + } - if (!file) { - return; + const ext = mime.extension(file.type); + if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) { + file.name = `${ file.name }.${ ext }`; + } + + return file; + }, + + getStore(modelName) { + const storageType = RocketChat.settings.get('FileUpload_Storage_Type'); + const handlerName = `${ storageType }:${ modelName }`; + + if (this.handlers[handlerName] == null) { + console.error(`Upload handler "${ handlerName }" does not exists`); + } + + return this.handlers[handlerName]; + }, + + get(file, req, res, next) { + if (file.store && this.handlers && this.handlers[file.store] && this.handlers[file.store].get) { + this.handlers[file.store].get(file, req, res, next); + } else { + res.writeHead(404); + res.end(); + return; + } } +}); + - this.handlers[file.store].delete(file); +export class FileUploadClass { + constructor({ name, model, store, get, insert, getStore }) { + this.name = name; + this.model = model || this.getModelFromName(); + this._store = store; + this.get = get; + this.insert = insert; - return RocketChat.models.Uploads.deleteFile(file._id); -}; + if (getStore) { + this.getStore = getStore; + } -FileUpload.get = function(file, req, res, next) { - if (file.store && this.handlers && this.handlers[file.store] && this.handlers[file.store].get) { - this.handlers[file.store].get.call(this, file, req, res, next); - } else { - res.writeHead(404); - res.end(); - return; + FileUpload.handlers[name] = this; } -}; -FileUpload.addExtensionTo = function(file) { - if (mime.lookup(file.name) === file.type) { - return file; + getStore() { + return this._store; } - const ext = mime.extension(file.type); - if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) { - file.name = `${ file.name }.${ ext }`; + get store() { + return this.getStore(); } - return file; -}; + set store(store) { + this._store = store; + } -FileUpload.avatarTransformWrite = function(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { - return readStream.pipe(writeStream); + getModelFromName() { + return RocketChat.models[this.name.split(':')[1]]; } - const height = RocketChat.settings.get('Accounts_AvatarSize'); - const width = height; - return RocketChatFile.gm(readStream).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); -}; + delete(fileId) { + if (this.store && this.store.delete) { + this.store.delete(fileId); + } + + return this.model.deleteFile(fileId); + } + + deleteById(fileId) { + const file = this.model.findOneById(fileId); + + if (!file) { + return; + } + + return this.delete(file._id); + } + + deleteByName(fileName) { + const file = this.model.findOneByName(fileName); + + if (!file) { + return; + } + + return this.delete(file._id); + } +} diff --git a/packages/rocketchat-lib/server/functions/deleteMessage.js b/packages/rocketchat-lib/server/functions/deleteMessage.js index 7a190964be..50fd599986 100644 --- a/packages/rocketchat-lib/server/functions/deleteMessage.js +++ b/packages/rocketchat-lib/server/functions/deleteMessage.js @@ -21,7 +21,7 @@ RocketChat.deleteMessage = function(message, user) { } if (message.file && message.file._id) { - FileUpload.delete(message.file._id); + FileUpload.getStore('Uploads').deleteById(message.file._id); } Meteor.defer(function() { diff --git a/packages/rocketchat-lib/server/functions/deleteUser.js b/packages/rocketchat-lib/server/functions/deleteUser.js index a139389b06..ab7ec6719b 100644 --- a/packages/rocketchat-lib/server/functions/deleteUser.js +++ b/packages/rocketchat-lib/server/functions/deleteUser.js @@ -1,4 +1,3 @@ -/* globals RocketChat */ RocketChat.deleteUser = function(userId) { const user = RocketChat.models.Users.findOneById(userId); @@ -22,7 +21,7 @@ RocketChat.deleteUser = function(userId) { // removes user's avatar if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url') { - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); + FileUpload.getStore('Avatars').deleteByName(user.username); } RocketChat.models.Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. diff --git a/packages/rocketchat-lib/server/functions/setUserAvatar.js b/packages/rocketchat-lib/server/functions/setUserAvatar.js index e80a8c7cdd..40289080a9 100644 --- a/packages/rocketchat-lib/server/functions/setUserAvatar.js +++ b/packages/rocketchat-lib/server/functions/setUserAvatar.js @@ -40,13 +40,18 @@ RocketChat.setUserAvatar = function(user, dataURI, contentType, service) { } const rs = RocketChatFile.bufferToStream(new Buffer(image, encoding)); - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ user.username }.jpg`), contentType); - ws.on('end', Meteor.bindEnvironment(function() { + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); + + const file = { + userId: user._id, + type: contentType + }; + + fileStore.insert(file, rs, () => { Meteor.setTimeout(function() { RocketChat.models.Users.setAvatarOrigin(user._id, service); RocketChat.Notifications.notifyLogged('updateAvatar', {username: user.username}); }, 500); - })); - rs.pipe(ws); + }); }; diff --git a/server/methods/deleteFileMessage.js b/server/methods/deleteFileMessage.js index ce10e596c4..9ffc1fe7f8 100644 --- a/server/methods/deleteFileMessage.js +++ b/server/methods/deleteFileMessage.js @@ -9,6 +9,6 @@ Meteor.methods({ return Meteor.call('deleteMessage', msg); } - return FileUpload.delete(fileID); + return FileUpload.getStore('Uploads').deleteById(fileID); } }); diff --git a/server/methods/resetAvatar.js b/server/methods/resetAvatar.js index 8b5d1fed21..ea551e5085 100644 --- a/server/methods/resetAvatar.js +++ b/server/methods/resetAvatar.js @@ -13,7 +13,7 @@ Meteor.methods({ } const user = Meteor.user(); - RocketChatFileAvatarInstance.deleteFile(`${ user.username }.jpg`); + FileUpload.getStore('Avatars').deleteByName(user.username); RocketChat.models.Users.unsetAvatarOrigin(user._id); RocketChat.Notifications.notifyLogged('updateAvatar', { username: user.username diff --git a/server/startup/initialData.js b/server/startup/initialData.js index 298d76c191..b8f9acdcaf 100644 --- a/server/startup/initialData.js +++ b/server/startup/initialData.js @@ -21,16 +21,19 @@ Meteor.startup(function() { RocketChat.authz.addUserRoles('rocket.cat', 'bot'); const rs = RocketChatFile.bufferToStream(new Buffer(Assets.getBinary('avatars/rocketcat.png'), 'utf8')); + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName('rocket.cat'); - RocketChatFileAvatarInstance.deleteFile('rocket.cat.jpg'); - - const ws = RocketChatFileAvatarInstance.createWriteStream('rocket.cat.jpg', 'image/png'); - - ws.on('end', Meteor.bindEnvironment(function() { - return RocketChat.models.Users.setAvatarOrigin('rocket.cat', 'local'); - })); + const file = { + userId: 'rocket.cat', + type: 'image/png' + }; - rs.pipe(ws); + Meteor.runAsUser('rocket.cat', () => { + fileStore.insert(file, rs, () => { + return RocketChat.models.Users.setAvatarOrigin('rocket.cat', 'local'); + }); + }); } if (process.env.ADMIN_PASS) { -- GitLab From 001dd3a72721c2fa654e5c50cd64de6d3a35691d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 2 May 2017 09:19:21 -0300 Subject: [PATCH 031/280] Add FileUpload to .eslintrc --- .eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc b/.eslintrc index bbae28a07d..2c18466035 100644 --- a/.eslintrc +++ b/.eslintrc @@ -105,6 +105,7 @@ "EJSON" : false, "Email" : false, "FlowRouter" : false, + "FileUpload" : false, "HTTP" : false, "getNextAgent" : false, "handleError" : false, -- GitLab From 8360b869b875ff0b31ede1ee602f40ed86a028b0 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 2 May 2017 09:20:14 -0300 Subject: [PATCH 032/280] Fix ESLint error --- .../server/config/configFileUploadGridFS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js b/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js index 4e7553f642..e26b7cc7d6 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js @@ -156,7 +156,7 @@ new FileUploadClass({ new FileUploadClass({ name: 'GridFS:Avatars', getStore() { - return Meteor.fileStoreAvatar + return Meteor.fileStoreAvatar; }, get(file, req, res) { -- GitLab From 06d966cd8bf049b9c6abbbd088dc93b09e341fe0 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 2 May 2017 10:09:02 -0300 Subject: [PATCH 033/280] Remove RocketChatFileAvatarInstance --- packages/rocketchat-ldap/server/sync.js | 16 ++++++--- .../server/functions/setUsername.coffee | 10 ++---- server/startup/avatar.js | 34 ------------------- server/startup/migrations/v002.js | 14 +++++--- 4 files changed, 23 insertions(+), 51 deletions(-) diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index df3ec4bf82..cdac0c7691 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -143,16 +143,22 @@ syncUserData = function syncUserData(user, ldapUser) { const avatar = ldapUser.raw.thumbnailPhoto || ldapUser.raw.jpegPhoto; if (avatar) { logger.info('Syncing user avatar'); + const rs = RocketChatFile.bufferToStream(avatar); - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ user.username }.jpg`), 'image/jpeg'); - ws.on('end', Meteor.bindEnvironment(function() { + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); + + const file = { + userId: user._id, + type: 'image/jpeg' + }; + + fileStore.insert(file, rs, () => { Meteor.setTimeout(function() { RocketChat.models.Users.setAvatarOrigin(user._id, 'ldap'); RocketChat.Notifications.notifyLogged('updateAvatar', {username: user.username}); }, 500); - })); - rs.pipe(ws); + }); } } }; diff --git a/packages/rocketchat-lib/server/functions/setUsername.coffee b/packages/rocketchat-lib/server/functions/setUsername.coffee index a3afa4a49e..f07e632753 100644 --- a/packages/rocketchat-lib/server/functions/setUsername.coffee +++ b/packages/rocketchat-lib/server/functions/setUsername.coffee @@ -61,13 +61,9 @@ RocketChat._setUsername = (userId, username) -> RocketChat.models.Subscriptions.setUserUsernameByUserId user._id, username RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName previousUsername, username - rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent("#{previousUsername}.jpg")) - if rs? - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{username}.jpg") - ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{username}.jpg"), rs.contentType - ws.on 'end', Meteor.bindEnvironment -> - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{previousUsername}.jpg") - rs.readStream.pipe(ws) + fileStore = FileUpload.getStore('Avatars') + file = fileStore.model.findOneByName(previousUsername) + fileStore.model.updateFileNameById(file._id, username) # Set new username RocketChat.models.Users.setUsername user._id, username diff --git a/server/startup/avatar.js b/server/startup/avatar.js index ae69381aff..0fa118f685 100644 --- a/server/startup/avatar.js +++ b/server/startup/avatar.js @@ -1,39 +1,5 @@ /* globals FileUpload */ Meteor.startup(function() { - let storeType = 'GridFS'; - - if (RocketChat.settings.get('Accounts_AvatarStoreType')) { - storeType = RocketChat.settings.get('Accounts_AvatarStoreType'); - } - - const RocketChatStore = RocketChatFile[storeType]; - - if (!RocketChatStore) { - throw new Error(`Invalid RocketChatStore type [${ storeType }]`); - } - - console.log((`Using ${ storeType } for Avatar storage`).green); - - function transformWrite(file, readStream, writeStream) { - if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { - return readStream.pipe(writeStream); - } - const height = RocketChat.settings.get('Accounts_AvatarSize'); - const width = height; - return RocketChatFile.gm(readStream, file.fileName).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); - } - - let path = '~/uploads'; - if (RocketChat.settings.get('Accounts_AvatarStorePath') && RocketChat.settings.get('Accounts_AvatarStorePath').trim() !== '') { - path = RocketChat.settings.get('Accounts_AvatarStorePath'); - } - - this.RocketChatFileAvatarInstance = new RocketChatStore({ - name: 'avatars', - absolutePath: path, - transformWrite - }); - WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/*, next*/) { const params = { username: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) diff --git a/server/startup/migrations/v002.js b/server/startup/migrations/v002.js index ad2d589a3c..e67336ed64 100644 --- a/server/startup/migrations/v002.js +++ b/server/startup/migrations/v002.js @@ -26,13 +26,17 @@ RocketChat.Migrations.add({ const {image, contentType} = RocketChatFile.dataURIParse(dataURI); const rs = RocketChatFile.bufferToStream(new Buffer(image, 'base64')); - const ws = RocketChatFileAvatarInstance.createWriteStream(`${ user.username }.jpg`, contentType); + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); - ws.on('end', Meteor.bindEnvironment(function() { - return RocketChat.models.Users.setAvatarOrigin(user._id, service); - })); + const file = { + userId: user._id, + type: contentType + }; - return rs.pipe(ws); + fileStore.insert(file, rs, () => { + return RocketChat.models.Users.setAvatarOrigin(user._id, service); + }); }); } }); -- GitLab From 014241c352eaf0e694e0f869b76f63653cd31d20 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 2 May 2017 12:39:30 -0300 Subject: [PATCH 034/280] Make Amazon S3 work again --- .../client/lib/FileUploadAmazonS3.js | 3 +- .../server/config/configFileUploadAmazonS3.js | 48 +++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js index 0c6f3efd90..6472ad7bbe 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js @@ -7,6 +7,7 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { 'upload': 'rocketchat-uploads', 'avatar': 'rocketchat-avatars' }; + this.directive = directive; this.uploader = new Slingshot.Upload(directives[directive], meta); } @@ -23,7 +24,7 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; - return callback(null, file, 's3'); + return callback(null, file, this.directive === 'avatar' ? 'S3:Avatars' : 'S3:Uploads'); } }); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index fe18ecaace..c4f3e13e0c 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -17,27 +17,37 @@ const generateURL = function(file) { return `${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; }; +const getFile = function(file, req, res) { + const fileUrl = generateURL(file); + + if (fileUrl) { + res.setHeader('Location', fileUrl); + res.writeHead(302); + } + res.end(); +}; + +const deleteFile = function(file) { + const s3 = new AWS.S3(); + const request = s3.deleteObject({ + Bucket: file.s3.bucket, + Key: file.s3.path + file._id + }); + request.send(); +}; + new FileUploadClass({ name: 'S3:Uploads', - get(file, req, res) { - const fileUrl = generateURL(file); + get: getFile, + delete: deleteFile +}); - if (fileUrl) { - res.setHeader('Location', fileUrl); - res.writeHead(302); - } - res.end(); - }, - - delete(file) { - const s3 = new AWS.S3(); - const request = s3.deleteObject({ - Bucket: file.s3.bucket, - Key: file.s3.path + file._id - }); - request.send(); - } +new FileUploadClass({ + name: 'S3:Avatars', + + get: getFile, + delete: deleteFile }); function createDirective(directiveName, { key, bucket, accessKey, secretKey, region, acl, cdn, bucketUrl}) { @@ -89,7 +99,7 @@ const configureSlingshot = _.debounce(() => { path } }; - const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 's3', file, upload); + const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 'S3:Uploads', file, upload); return path + fileId; } @@ -110,7 +120,7 @@ const configureSlingshot = _.debounce(() => { } }; delete file.name; - RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 's3', file, upload); + RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 'S3:Avatars', file, upload); return path + user.username; } -- GitLab From 451b055a70e375214cf723f6ef1d7392113b42a5 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 2 May 2017 19:51:45 -0300 Subject: [PATCH 035/280] Fix upload for Google Cloud Storage --- .../client/lib/FileUploadAmazonS3.js | 2 +- .../client/lib/FileUploadGoogleStorage.js | 2 +- .../server/config/configFileUploadAmazonS3.js | 8 +-- .../config/configFileUploadGoogleStorage.js | 60 +++++++++++-------- .../rocketchat-ui/client/lib/fileUpload.js | 2 +- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js index 6472ad7bbe..678830d2bf 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js @@ -24,7 +24,7 @@ FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; - return callback(null, file, this.directive === 'avatar' ? 'S3:Avatars' : 'S3:Uploads'); + return callback(null, file, this.directive === 'avatar' ? 'AmazonS3:Avatars' : 'AmazonS3:Uploads'); } }); diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js index b4b047c42b..a5aa21485b 100644 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js @@ -23,7 +23,7 @@ FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileU file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); file.url = downloadUrl; - return callback(null, file, 'googleCloudStorage'); + return callback(null, file, 'GoogleCloudStorage:Uploads'); } }); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index c4f3e13e0c..b954f95e71 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -37,14 +37,14 @@ const deleteFile = function(file) { }; new FileUploadClass({ - name: 'S3:Uploads', + name: 'AmazonS3:Uploads', get: getFile, delete: deleteFile }); new FileUploadClass({ - name: 'S3:Avatars', + name: 'AmazonS3:Avatars', get: getFile, delete: deleteFile @@ -99,7 +99,7 @@ const configureSlingshot = _.debounce(() => { path } }; - const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 'S3:Uploads', file, upload); + const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 'AmazonS3:Uploads', file, upload); return path + fileId; } @@ -120,7 +120,7 @@ const configureSlingshot = _.debounce(() => { } }; delete file.name; - RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 'S3:Avatars', file, upload); + RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 'AmazonS3:Avatars', file, upload); return path + user.username; } diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js index 69e0b514ba..c9cd841aac 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js @@ -63,36 +63,46 @@ function createDirective(directiveName, { key, bucket, accessId, secret }) { } } -new FileUploadClass({ - name: 'googleCloudStorage', +const getFile = function(file, req, res) { + const fileUrl = generateGetURL({ file }); - get(file, req, res) { - const fileUrl = generateGetURL({ file }); + if (fileUrl) { + res.setHeader('Location', fileUrl); + res.writeHead(302); + } + res.end(); +}; - if (fileUrl) { - res.setHeader('Location', fileUrl); - res.writeHead(302); - } - res.end(); - }, +const deleteFile = function(file) { + if (!file || !file.googleCloudStorage) { + console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, the file and googleCloudStorage properties are not defined.'); + return; + } - delete(file) { - if (!file || !file.googleCloudStorage) { - console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, the file and googleCloudStorage properties are not defined.'); - return; - } + // RocketChat.models.Uploads.deleteFile(file._id); + + const url = generateDeleteUrl({ file }); + + if (_.isEmpty(url)) { + console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, failed to generate a delete url.'); + return; + } - // RocketChat.models.Uploads.deleteFile(file._id); + HTTP.call('DELETE', url); +}; - const url = generateDeleteUrl({ file }); +new FileUploadClass({ + name: 'GoogleCloudStorage:Uploads', - if (_.isEmpty(url)) { - console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, failed to generate a delete url.'); - return; - } + get: getFile, + delete: deleteFile +}); - HTTP.call('DELETE', url); - } +new FileUploadClass({ + name: 'GoogleCloudStorage:Avatars', + + get: getFile, + delete: deleteFile }); const createGoogleStorageDirective = _.debounce(() => { @@ -108,7 +118,7 @@ const createGoogleStorageDirective = _.debounce(() => { path } }; - const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 'googleCloudStorage', file, upload); + const fileId = RocketChat.models.Uploads.insertFileInit(this.userId, 'GoogleCloudStorage:Uploads', file, upload); return path + fileId; } @@ -128,7 +138,7 @@ const createGoogleStorageDirective = _.debounce(() => { } }; delete file.name; - RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 'googleCloudStorage', file, upload); + RocketChat.models.Avatars.insertAvatarFileInit(user.username, this.userId, 'GoogleCloudStorage:Avatars', file, upload); return path + user.username; } diff --git a/packages/rocketchat-ui/client/lib/fileUpload.js b/packages/rocketchat-ui/client/lib/fileUpload.js index 20d2007951..5f8cb982fa 100644 --- a/packages/rocketchat-ui/client/lib/fileUpload.js +++ b/packages/rocketchat-ui/client/lib/fileUpload.js @@ -196,7 +196,7 @@ fileUpload = function(filesToUpload) { const uploading = Session.get('uploading'); if (uploading !== null) { const item = _.findWhere(uploading, { - id: this.id + id: upload.id }); return Session.set('uploading', _.without(uploading, item)); } -- GitLab From f332ef922171952d168aab254bfa957e32283dc2 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 4 May 2017 15:58:32 -0300 Subject: [PATCH 036/280] More Work In Progress --- packages/rocketchat-ui-admin/client/admin.js | 78 ++---- .../rocketchat-ui-admin/client/adminFlex.js | 82 ++++++ .../rocketchat-ui-admin/client/adminInfo.js | 87 ++++++ .../client/rooms/adminRoomInfo.js | 265 ++++++++++++++++++ .../client/users/adminInviteUser.js | 46 +++ .../client/users/adminUserChannels.js | 27 ++ .../client/users/adminUsers.js | 141 ++++++++++ packages/rocketchat-ui-admin/package.js | 8 +- 8 files changed, 676 insertions(+), 58 deletions(-) create mode 100644 packages/rocketchat-ui-admin/client/adminFlex.js create mode 100644 packages/rocketchat-ui-admin/client/adminInfo.js create mode 100644 packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js create mode 100644 packages/rocketchat-ui-admin/client/users/adminInviteUser.js create mode 100644 packages/rocketchat-ui-admin/client/users/adminUserChannels.js create mode 100644 packages/rocketchat-ui-admin/client/users/adminUsers.js diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js index 7694345596..56f59ba917 100644 --- a/packages/rocketchat-ui-admin/client/admin.js +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -21,7 +21,7 @@ const setFieldValue = function(settingId, value, type, editor) { input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change(); input.val(value).change(); if (editor === 'color') { - return new jscolor(input); + return new jscolor(input); //eslint-disable-line } break; default: @@ -131,36 +131,13 @@ Template.admin.helpers({ } }); } - if (sections[setting.section || ''] == null) { - sections[setting.section] = []; + const settingSection = setting.section || ''; + if (sections[settingSection] == null) { + sections[settingSection] = []; } - sections[setting.section || ''].push(setting); + sections[settingSection].push(setting); }); - Object.keys(settings).forEach((key) =>{ - const setting = settings[key]; - if (setting.i18nDefaultQuery != null) { - if (_.isString(setting.i18nDefaultQuery)) { - i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery); - } else { - i18nDefaultQuery = setting.i18nDefaultQuery; - } - if (!_.isArray(i18nDefaultQuery)) { - i18nDefaultQuery = [i18nDefaultQuery]; - } - Object.keys(i18nDefaultQuery).forEach((key) => { - const item = i18nDefaultQuery[key]; - if (RocketChat.settings.collectionPrivate.findOne(item) != null) { - setting.value = TAPi18n.__(`${ setting._id }_Default`); - } - }); - - } - if (sections[setting.section || ''] == null) { - sections[setting.section] = []; - } - sections[setting.section || ''].push(setting); - }); group.sections = []; Object.keys(sections).forEach((key) =>{ const value = sections[key]; @@ -548,7 +525,6 @@ Template.admin.events({ const name = this.section.replace('Custom OAuth: ', ''); const config = { title: TAPi18n.__('Are_you_sure'), - type: 'input', type: 'warning', showCancelButton: true, confirmButtonColor: '#DD6B55', @@ -574,14 +550,14 @@ Template.admin.events({ } } const results = []; - for (i = 0, len = files.length; i < len; i++) { - blob = files[i]; + Object.keys(files).forEach((key) => { + const blob = files[key]; toastr.info(TAPi18n.__('Uploading_file')); - reader = new FileReader(); + const reader = new FileReader(); reader.readAsBinaryString(blob); - results.push(reader.onloadend = (function(_this) { + results.push(reader.onloadend = () => { return function() { - return Meteor.call('setAsset', reader.result, blob.type, _this.asset, function(err, data) { + return Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) { if (err != null) { handleError(err); console.log(err); @@ -590,8 +566,8 @@ Template.admin.events({ return toastr.success(TAPi18n.__('File_uploaded')); }); }; - }(this))); - } + }); + }); return results; }, 'click .expand'(e) { @@ -605,12 +581,11 @@ Template.admin.events({ $(e.currentTarget).closest('.section').addClass('section-collapsed'); return $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); }, - 'click button.action'(e) { + 'click button.action'() { if (this.type !== 'action') { return; } return Meteor.call(this.value, function(err, data) { - let args; if (err != null) { err.details = _.extend(err.details || {}, { errorTitle: 'Error' @@ -618,28 +593,25 @@ Template.admin.events({ handleError(err); return; } - args = [data.message].concat(data.params); + const args = [data.message].concat(data.params); return toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success')); }); }, 'click .button-fullscreen'() { - let codeMirrorBox; - codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); + const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color'); return codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); }, 'click .button-restore'() { - let codeMirrorBox; - codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); + const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color'); return codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); }, 'autocompleteselect .autocomplete'(event, instance, doc) { - let selectedRooms, value; - selectedRooms = instance.selectedRooms.get(); + const selectedRooms = instance.selectedRooms.get(); selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc); instance.selectedRooms.set(selectedRooms); - value = selectedRooms[this.id]; + const value = selectedRooms[this.id]; TempSettings.update({ _id: this.id }, { @@ -652,15 +624,14 @@ Template.admin.events({ return event.currentTarget.focus(); }, 'click .remove-room'(event, instance) { - let docId, selectedRooms, settingId, value; - docId = this._id; - settingId = event.currentTarget.getAttribute('data-setting'); - selectedRooms = instance.selectedRooms.get(); + const docId = this._id; + const settingId = event.currentTarget.getAttribute('data-setting'); + const selectedRooms = instance.selectedRooms.get(); selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], function(setting) { return setting._id === docId; }); instance.selectedRooms.set(selectedRooms); - value = selectedRooms[settingId]; + const value = selectedRooms[settingId]; return TempSettings.update({ _id: settingId }, { @@ -678,8 +649,7 @@ Template.admin.onRendered(function() { return SideNav.openFlex(); }); return Tracker.autorun(function() { - let hasColor; - hasColor = TempSettings.findOne({ + const hasColor = TempSettings.findOne({ group: FlowRouter.getParam('group'), type: 'color' }, { @@ -690,7 +660,7 @@ Template.admin.onRendered(function() { if (hasColor) { return Meteor.setTimeout(function() { return $('.colorpicker-input').each(function(index, el) { - return new jscolor(el); + return new jscolor(el); //eslint-disable-line }); }, 400); } diff --git a/packages/rocketchat-ui-admin/client/adminFlex.js b/packages/rocketchat-ui-admin/client/adminFlex.js new file mode 100644 index 0000000000..8c5c5da192 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/adminFlex.js @@ -0,0 +1,82 @@ +/*globals menu */ +Template.adminFlex.onCreated(function() { + this.settingsFilter = new ReactiveVar(''); + if (RocketChat.settings.cachedCollectionPrivate == null) { + RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ + name: 'private-settings', + eventType: 'onLogged' + }); + RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection; + return RocketChat.settings.cachedCollectionPrivate.init(); + } +}); + +const label = function() { + return TAPi18n.__(this.i18nLabel || this._id); +}; + +Template.adminFlex.helpers({ + groups() { + const filter = Template.instance().settingsFilter.get(); + const query = { + type: 'group' + }; + if (filter) { + const filterRegex = new RegExp(_.escapeRegExp(filter), 'i'); + const records = RocketChat.settings.collectionPrivate.find().fetch(); + let groups = []; + records.forEach(function(record) { + if (filterRegex.test(TAPi18n.__(record.i18nLabel || record._id))) { + groups.push(record.group || record._id); + } + }); + groups = _.unique(groups); + if (groups.length > 0) { + query._id = { + $in: groups + }; + } + } + RocketChat.settings.collectionPrivate.find(query).fetch().map((al) => { + return function(el) { + console.log(el); + console.log(al); + el.label = label.apply(el); + return el; + }; + }).sort(() => { + return function(a, b) { + if (a.label.toLowerCase() >= b.label.toLowerCase()) { + return 1; + } else { + return -1; + } + }; + }); + }, + label, + adminBoxOptions() { + return RocketChat.AdminBox.getOptions(); + } +}); + +Template.adminFlex.events({ + 'mouseenter header'() { + return SideNav.overArrow(); + }, + 'mouseleave header'() { + return SideNav.leaveArrow(); + }, + 'click header'() { + return SideNav.closeFlex(); + }, + 'click .cancel-settings'() { + return SideNav.closeFlex(); + }, + 'click .admin-link'() { + return menu.close(); + }, + 'keyup [name=settings-search]'(e, t) { + return t.settingsFilter.set(e.target.value); + } +}); diff --git a/packages/rocketchat-ui-admin/client/adminInfo.js b/packages/rocketchat-ui-admin/client/adminInfo.js new file mode 100644 index 0000000000..2e527fe21b --- /dev/null +++ b/packages/rocketchat-ui-admin/client/adminInfo.js @@ -0,0 +1,87 @@ +import moment from 'moment'; + +Template.adminInfo.helpers({ + isReady() { + return Template.instance().ready.get(); + }, + statistics() { + return Template.instance().statistics.get(); + }, + inGB(size) { + if (size > 1073741824) { + return `${ _.numberFormat(size / 1024 / 1024 / 1024, 2) } GB`; + } + return `${ _.numberFormat(size / 1024 / 1024, 2) } MB`; + }, + humanReadableTime(time) { + const days = Math.floor(time / 86400); + const hours = Math.floor((time % 86400) / 3600); + const minutes = Math.floor(((time % 86400) % 3600) / 60); + const seconds = Math.floor(((time % 86400) % 3600) % 60); + let out = ''; + if (days > 0) { + out += `${ days } ${ TAPi18n.__('days') }, `; + } + if (hours > 0) { + out += `${ hours } ${ TAPi18n.__('hours') }, `; + } + if (minutes > 0) { + out += `${ minutes } ${ TAPi18n.__('minutes') }, `; + } + if (seconds > 0) { + out += `${ seconds } ${ TAPi18n.__('seconds') }`; + } + return out; + }, + formatDate(date) { + if (date) { + return moment(date).format('LLL'); + } + }, + numFormat(number) { + return _.numberFormat(number, 2); + }, + info() { + return RocketChat.Info; + }, + build() { + return RocketChat.Info && RocketChat.Info.compile || RocketChat.Info && RocketChat.Info.build; + } +}); + +Template.adminInfo.events({ + 'click .refresh'(e, instance) { + instance.ready.set(false); + return Meteor.call('getStatistics', true, function(error, statistics) { + instance.ready.set(true); + if (error) { + return handleError(error); + } else { + return instance.statistics.set(statistics); + } + }); + } +}); + +Template.adminInfo.onRendered(function() { + return Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + return SideNav.openFlex(); + }); +}); + +Template.adminInfo.onCreated(function() { + const instance = this; + this.statistics = new ReactiveVar({}); + this.ready = new ReactiveVar(false); + if (RocketChat.authz.hasAllPermission('view-statistics')) { + return Meteor.call('getStatistics', function(error, statistics) { + instance.ready.set(true); + if (error) { + return handleError(error); + } else { + return instance.statistics.set(statistics); + } + }); + } +}); diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js b/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js new file mode 100644 index 0000000000..44434a6319 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js @@ -0,0 +1,265 @@ +/*globals AdminChatRoom */ +import toastr from 'toastr'; +Template.adminRoomInfo.helpers({ + selectedRoom() { + return Session.get('adminRoomsSelected'); + }, + canEdit() { + return RocketChat.authz.hasAllPermission('edit-room', this.rid); + }, + editing(field) { + return Template.instance().editing.get() === field; + }, + notDirect() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + return room && room.t !== 'd'; + }, + roomType() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + return room && room.t; + }, + channelSettings() { + return RocketChat.ChannelSettings.getOptions(null, 'admin-room'); + }, + roomTypeDescription() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + const roomType = room && room.t; + if (roomType === 'c') { + return t('Channel'); + } else if (roomType === 'p') { + return t('Private_Group'); + } + }, + roomName() { + const room = AdminChatRoom.findOne(this.rid, { fields: { name: 1 } }); + console.log(room); + return room && room.name; + }, + roomTopic() { + const room = AdminChatRoom.findOne(this.rid, { fields: { topic: 1 } }); + return room && room.topic; + }, + archivationState() { + const room = AdminChatRoom.findOne(this.rid, { fields: { archived: 1 } }); + return room && room.archived; + }, + archivationStateDescription() { + const room = AdminChatRoom.findOne(this.rid, { fields: { archived: 1 } }); + const archivationState = room && room.archived; + if (archivationState === true) { + return t('Room_archivation_state_true'); + } else { + return t('Room_archivation_state_false'); + } + }, + canDeleteRoom() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + const roomType = room && room.t; + return (roomType != null) && RocketChat.authz.hasAtLeastOnePermission(`delete-${ roomType }`); + }, + readOnly() { + const room = AdminChatRoom.findOne(this.rid, { fields: { ro: 1 } }); + return room && room.ro; + }, + readOnlyDescription() { + const room = AdminChatRoom.findOne(this.rid, { fields: { ro: 1 } }); + const readOnly = room && room.ro; + + if (readOnly === true) { + return t('True'); + } else { + return t('False'); + } + } +}); + +Template.adminRoomInfo.events({ + 'click .delete'() { + return swal({ + title: t('Are_you_sure'), + text: t('Delete_Room_Warning'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes_delete_it'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: false + }, () => { + swal.disableButtons(); + return Meteor.call('eraseRoom', this.rid, function(error) { + if (error) { + handleError(error); + return swal.enableButtons(); + } else { + return swal({ + title: t('Deleted'), + text: t('Room_has_been_deleted'), + type: 'success', + timer: 2000, + showConfirmButton: false + }); + } + }); + }); + }, + 'keydown input[type=text]'(e, t) { + if (e.keyCode === 13) { + e.preventDefault(); + return t.saveSetting(this.rid); + } + }, + 'click [data-edit]'(e, t) { + e.preventDefault(); + t.editing.set($(e.currentTarget).data('edit')); + return setTimeout((function() { + return t.$('input.editing').focus().select(); + }), 100); + }, + 'click .cancel'(e, t) { + e.preventDefault(); + return t.editing.set(); + }, + 'click .save'(e, t) { + e.preventDefault(); + return t.saveSetting(this.rid); + } +}); + +Template.adminRoomInfo.onCreated(function() { + this.editing = new ReactiveVar; + this.validateRoomType = () => { + const type = this.$('input[name=roomType]:checked').val(); + if (type !== 'c' && type !== 'p') { + toastr.error(t('error-invalid-room-type', { + type + })); + } + return true; + }; + this.validateRoomName = (rid) => { + const room = AdminChatRoom.findOne(rid); + let nameValidation; + if (!RocketChat.authz.hasAllPermission('edit-room', rid) || (room.t !== 'c' && room.t !== 'p')) { + toastr.error(t('error-not-allowed')); + return false; + } + name = $('input[name=roomName]').val(); + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (_error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(name)) { + toastr.error(t('error-invalid-room-name', { + room_name: name + })); + return false; + } + return true; + }; + this.validateRoomTopic = () => { + return true; + }; + return this.saveSetting = (rid) => { + switch (this.editing.get()) { + case 'roomName': + if (this.validateRoomName(rid)) { + RocketChat.callbacks.run('roomNameChanged', AdminChatRoom.findOne(rid)); + Meteor.call('saveRoomSettings', rid, 'roomName', this.$('input[name=roomName]').val(), function(err) { + if (err) { + return handleError(err); + } + return toastr.success(TAPi18n.__('Room_name_changed_successfully')); + }); + } + break; + case 'roomTopic': + if (this.validateRoomTopic(rid)) { + Meteor.call('saveRoomSettings', rid, 'roomTopic', this.$('input[name=roomTopic]').val(), function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_topic_changed_successfully')); + return RocketChat.callbacks.run('roomTopicChanged', AdminChatRoom.findOne(rid)); + }); + } + break; + case 'roomAnnouncement': + if (this.validateRoomTopic(rid)) { + Meteor.call('saveRoomSettings', rid, 'roomAnnouncement', this.$('input[name=roomAnnouncement]').val(), function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_announcement_changed_successfully')); + return RocketChat.callbacks.run('roomAnnouncementChanged', AdminChatRoom.findOne(rid)); + }); + } + break; + case 'roomType': + const val = this.$('input[name=roomType]:checked').val(); + if (this.validateRoomType(rid)) { + RocketChat.callbacks.run('roomTypeChanged', AdminChatRoom.findOne(rid)); + const saveRoomSettings = function() { + return Meteor.call('saveRoomSettings', rid, 'roomType', val, function(err) { + if (err) { + return handleError(err); + } else { + return toastr.success(TAPi18n.__('Room_type_changed_successfully')); + } + }); + }; + if (!AdminChatRoom.findOne(rid, { + fields: { + 'default': 1 + } + })['default']) { + return saveRoomSettings(); + } + swal({ + title: t('Room_default_change_to_private_will_be_default_no_more'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes'), + cancelButtonText: t('Cancel'), + closeOnConfirm: true, + html: false + }, function(confirmed) { + return !confirmed || saveRoomSettings(); + }); + } + break; + case 'archivationState': + const room = AdminChatRoom.findOne(rid); + if (this.$('input[name=archivationState]:checked').val() === 'true') { + if (room && room.archived !== true) { + Meteor.call('archiveRoom', rid, function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_archived')); + return RocketChat.callbacks.run('archiveRoom', AdminChatRoom.findOne(rid)); + }); + } + } else if ((room && room.archived) === true) { + Meteor.call('unarchiveRoom', rid, function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_unarchived')); + return RocketChat.callbacks.run('unarchiveRoom', AdminChatRoom.findOne(rid)); + }); + } + break; + case 'readOnly': + Meteor.call('saveRoomSettings', rid, 'readOnly', this.$('input[name=readOnly]:checked').val() === 'true', function(err) { + if (err) { + return handleError(err); + } + return toastr.success(TAPi18n.__('Read_only_changed_successfully')); + }); + } + return this.editing.set(); + }; +}); diff --git a/packages/rocketchat-ui-admin/client/users/adminInviteUser.js b/packages/rocketchat-ui-admin/client/users/adminInviteUser.js new file mode 100644 index 0000000000..1c456bed96 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/users/adminInviteUser.js @@ -0,0 +1,46 @@ +import toastr from 'toastr'; +Template.adminInviteUser.helpers({ + isAdmin() { + return RocketChat.authz.hasRole(Meteor.userId(), 'admin'); + }, + inviteEmails() { + return Template.instance().inviteEmails.get(); + } +}); + +Template.adminInviteUser.events({ + 'click .send'(e, instance) { + const emails = $('#inviteEmails').val().split(/[\s,;]/); + const rfcMailPattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + const validEmails = _.compact(_.map(emails, function(email) { + if (rfcMailPattern.test(email)) { + return email; + } + })); + if (validEmails.length) { + return Meteor.call('sendInvitationEmail', validEmails, function(error, result) { + if (result) { + instance.clearForm(); + instance.inviteEmails.set(validEmails); + } + if (error) { + return handleError(error); + } + }); + } else { + return toastr.error(t('Send_invitation_email_error')); + } + }, + 'click .cancel'(e, instance) { + instance.clearForm(); + instance.inviteEmails.set([]); + return Template.currentData().tabBar.close(); + } +}); + +Template.adminInviteUser.onCreated(function() { + this.inviteEmails = new ReactiveVar([]); + return this.clearForm = function() { + return $('#inviteEmails').val(''); + }; +}); diff --git a/packages/rocketchat-ui-admin/client/users/adminUserChannels.js b/packages/rocketchat-ui-admin/client/users/adminUserChannels.js new file mode 100644 index 0000000000..65e3022c0b --- /dev/null +++ b/packages/rocketchat-ui-admin/client/users/adminUserChannels.js @@ -0,0 +1,27 @@ +Template.adminUserChannels.helpers({ + type() { + if (this.t === 'd') { + return 'at'; + } else if (this.t === 'p') { + return 'lock'; + } else { + return 'hash'; + } + }, + route() { + switch (this.t) { + case 'd': + return FlowRouter.path('direct', { + username: this.name + }); + case 'p': + return FlowRouter.path('group', { + name: this.name + }); + case 'c': + return FlowRouter.path('channel', { + name: this.name + }); + } + } +}); diff --git a/packages/rocketchat-ui-admin/client/users/adminUsers.js b/packages/rocketchat-ui-admin/client/users/adminUsers.js new file mode 100644 index 0000000000..14df389800 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/users/adminUsers.js @@ -0,0 +1,141 @@ +/* globals RocketChatTabBar */ +Template.adminUsers.helpers({ + isReady() { + const instance = Template.instance(); + return instance.ready && instance.ready.get(); + }, + users() { + return Template.instance().users(); + }, + isLoading() { + const instance = Template.instance(); + if (!(instance.ready && instance.ready.get())) { + return 'btn-loading'; + } + }, + hasMore() { + const instance = Template.instance(); + const users = instance.users(); + if (instance.limit && instance.limit.get() && users && users.length) { + return instance.limit.get() === users.length; + } + }, + emailAddress() { + return _.map(this.emails, function(e) { e.address; }).join(', '); + }, + flexData() { + return { + tabBar: Template.instance().tabBar, + data: Template.instance().tabBarData.get() + }; + } +}); + +Template.adminUsers.onCreated(function() { + const instance = this; + this.limit = new ReactiveVar(50); + this.filter = new ReactiveVar(''); + this.ready = new ReactiveVar(true); + this.tabBar = new RocketChatTabBar(); + this.tabBar.showGroup(FlowRouter.current().route.name); + this.tabBarData = new ReactiveVar; + RocketChat.TabBar.addButton({ + groups: ['admin-users'], + id: 'invite-user', + i18nTitle: 'Invite_Users', + icon: 'icon-paper-plane', + template: 'adminInviteUser', + order: 1 + }); + RocketChat.TabBar.addButton({ + groups: ['admin-users'], + id: 'add-user', + i18nTitle: 'Add_User', + icon: 'icon-plus', + template: 'adminUserEdit', + order: 2 + }); + RocketChat.TabBar.addButton({ + groups: ['admin-users'], + id: 'admin-user-info', + i18nTitle: 'User_Info', + icon: 'icon-user', + template: 'adminUserInfo', + order: 3 + }); + this.autorun(function() { + const filter = instance.filter.get(); + const limit = instance.limit.get(); + const subscription = instance.subscribe('fullUserData', filter, limit); + instance.ready.set(subscription.ready()); + }); + this.users = function() { + let filter; + let query; + + if (instance.filter && instance.filter.get()) { + filter = _.trim(instance.filter.get()); + } + + if (filter) { + const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); + query = { + $or: [ + { + username: filterReg + }, { + name: filterReg + }, { + 'emails.address': filterReg + } + ] + }; + } else { + query = {}; + } + query.type = { + $in: ['user', 'bot'] + }; + + const limit = instance.limit && instance.limit.get(); + return Meteor.users.find(query, { limit, sort: { username: 1, name: 1 } }).fetch(); + }; +}); + +Template.adminUsers.onRendered(function() { + return Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + return SideNav.openFlex(); + }); +}); + +Template.adminUsers.events({ + 'keydown #users-filter'(e) { + if (e.which === 13) { + e.stopPropagation(); + return e.preventDefault(); + } + }, + 'keyup #users-filter'(e, t) { + e.stopPropagation(); + e.preventDefault(); + return t.filter.set(e.currentTarget.value); + }, + 'click .user-info'(e, instance) { + e.preventDefault(); + instance.tabBarData.set(Meteor.users.findOne(this._id)); + return instance.tabBar.open('admin-user-info'); + }, + 'click .info-tabs button'(e) { + e.preventDefault(); + $('.info-tabs button').removeClass('active'); + $(e.currentTarget).addClass('active'); + $('.user-info-content').hide(); + return $($(e.currentTarget).attr('href')).show(); + }, + 'click .load-more'(e, t) { + e.preventDefault(); + e.stopPropagation(); + return t.limit.set(t.limit.get() + 50); + } +}); diff --git a/packages/rocketchat-ui-admin/package.js b/packages/rocketchat-ui-admin/package.js index a7c2791137..9fbe0aa75b 100644 --- a/packages/rocketchat-ui-admin/package.js +++ b/packages/rocketchat-ui-admin/package.js @@ -27,7 +27,7 @@ Package.onUse(function(api) { api.addFiles('client/rooms/adminRooms.html', 'client'); api.addFiles('client/rooms/adminRoomInfo.html', 'client'); - api.addFiles('client/rooms/adminRoomInfo.coffee', 'client'); + api.addFiles('client/rooms/adminRoomInfo.js', 'client'); api.addFiles('client/rooms/channelSettingsDefault.html', 'client'); api.addFiles('client/rooms/channelSettingsDefault.js', 'client'); @@ -38,15 +38,15 @@ Package.onUse(function(api) { api.addFiles('client/users/adminUsers.html', 'client'); // coffee files - api.addFiles('client/admin.coffee', 'client'); + api.addFiles('client/admin.js', 'client'); api.addFiles('client/adminFlex.coffee', 'client'); - api.addFiles('client/adminInfo.coffee', 'client'); + api.addFiles('client/adminInfo.js', 'client'); api.addFiles('client/rooms/adminRooms.coffee', 'client'); api.addFiles('client/users/adminInviteUser.coffee', 'client'); api.addFiles('client/users/adminUserChannels.coffee', 'client'); - api.addFiles('client/users/adminUsers.coffee', 'client'); + api.addFiles('client/users/adminUsers.js', 'client'); api.addFiles('publications/adminRooms.js', 'server'); -- GitLab From 9077e30f3f1fdf14f4948cc3e459662f33eaa977 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 4 May 2017 16:31:07 -0300 Subject: [PATCH 037/280] wip on adminRooms.js --- .../client/rooms/adminRooms.js | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 packages/rocketchat-ui-admin/client/rooms/adminRooms.js diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRooms.js b/packages/rocketchat-ui-admin/client/rooms/adminRooms.js new file mode 100644 index 0000000000..a566500ec8 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/rooms/adminRooms.js @@ -0,0 +1,171 @@ +/*globals RocketChatTabBar */ +this.AdminChatRoom = new Mongo.Collection('rocketchat_room'); + +Template.adminRooms.helpers({ + isReady() { + const instance = Template.instance(); + return instance.ready && instance.ready.get(); + }, + rooms() { + return Template.instance().rooms(); + }, + isLoading() { + const instance = Template.instance(); + if (!(instance.ready && instance.ready.get())) { + return 'btn-loading'; + } + }, + hasMore() { + const instance = Template.instance(); + if (instance.limit && instance.limit.get() && instance.rooms() && instance.rooms().count()) { + return instance.limit.get() === instance.rooms().count(); + } + }, + roomCount() { + const instance = Template.instance(); + return instance.rooms() && instance.rooms().count(); + }, + name() { + if (this.t === 'c' || this.t === 'p') { + return this.name; + } else if (this.t === 'd') { + return this.usernames.join(' x '); + } + }, + type() { + if (this.t === 'c') { + return TAPi18n.__('Channel'); + } else if (this.t === 'd') { + return TAPi18n.__('Direct Message'); + } + if (this.t === 'p') { + return TAPi18n.__('Private Group'); + } + }, + 'default'() { + if (this['default']) { + return t('True'); + } else { + return t('False'); + } + }, + flexData() { + return { + tabBar: Template.instance().tabBar + }; + } +}); + +Template.adminRooms.onCreated(function() { + const instance = this; + this.limit = new ReactiveVar(50); + this.filter = new ReactiveVar(''); + this.types = new ReactiveVar([]); + this.ready = new ReactiveVar(true); + this.tabBar = new RocketChatTabBar(); + this.tabBar.showGroup(FlowRouter.current().route.name); + RocketChat.TabBar.addButton({ + groups: ['admin-rooms'], + id: 'admin-room', + i18nTitle: 'Room_Info', + icon: 'icon-info-circled', + template: 'adminRoomInfo', + order: 1 + }); + RocketChat.ChannelSettings.addOption({ + group: ['admin-room'], + id: 'make-default', + template: 'channelSettingsDefault', + data() { + return Session.get('adminRoomsSelected'); + }, + validation() { + return RocketChat.authz.hasAllPermission('view-room-administration'); + } + }); + this.autorun(function() { + const filter = instance.filter.get(); + let types = instance.types.get(); + if (types.length === 0) { + types = ['c', 'd', 'p']; + } + const limit = instance.limit.get(); + const subscription = instance.subscribe('adminRooms', filter, types, limit); + return instance.ready.set(subscription.ready()); + }); + this.rooms = function() { + let filter = _.trim((ref = instance.filter) != null ? ref.get() : void 0); + let types = (ref1 = instance.types) != null ? ref1.get() : void 0; + if (!_.isArray(types)) { + types = []; + } + let query = {}; + filter = _.trim(filter); + if (filter) { + const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); + query = { + $or: [ + { + name: filterReg + }, { + t: 'd', + usernames: filterReg + } + ] + }; + } + if (types.length) { + query['t'] = { + $in: types + }; + } + return AdminChatRoom.find(query, { + limit: (ref2 = instance.limit) != null ? ref2.get() : void 0, + sort: { + 'default': -1, + name: 1 + } + }); + }; + return this.getSearchTypes = function() { + return _.map($('[name=room-type]:checked'), function(input) { + return $(input).val(); + }); + }; +}); + +Template.adminRooms.onRendered(function() { + return Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + return SideNav.openFlex(); + }); +}); + +Template.adminRooms.events({ + 'keydown #rooms-filter'(e) { + if (e.which === 13) { + e.stopPropagation(); + return e.preventDefault(); + } + }, + 'keyup #rooms-filter'(e, t) { + e.stopPropagation(); + e.preventDefault(); + return t.filter.set(e.currentTarget.value); + }, + 'click .room-info'(e, instance) { + e.preventDefault(); + Session.set('adminRoomsSelected', { + rid: this._id + }); + return instance.tabBar.open('admin-room'); + }, + 'click .load-more'(e, t) { + e.preventDefault(); + e.stopPropagation(); + return t.limit.set(t.limit.get() + 50); + }, + 'change [name=room-type]'(e, t) { + return t.types.set(t.getSearchTypes()); + } +}); -- GitLab From 78bfa9182dce3ec651f61b91b73942008444b1f8 Mon Sep 17 00:00:00 2001 From: Aaron Ogle Date: Thu, 4 May 2017 17:07:01 -0500 Subject: [PATCH 038/280] Fix snap restore command --- .snapcraft/resources/restoredb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.snapcraft/resources/restoredb b/.snapcraft/resources/restoredb index cedf9e1358..0a204e8377 100755 --- a/.snapcraft/resources/restoredb +++ b/.snapcraft/resources/restoredb @@ -29,7 +29,7 @@ function ask_backup { read choice [[ "${choice,,}" = n* ]] && return - [[ "${choice,,}" = y* ]] && backupdb.sh && return + [[ "${choice,,}" = y* ]] && backupdb && return exit } -- GitLab From 59149959d2901c35a04e339d3ac267191af9aabd Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 5 May 2017 18:10:36 -0300 Subject: [PATCH 039/280] fix files --- .../rocketchat-ui-admin/client/adminFlex.js | 27 +++++++++---------- .../client/rooms/adminRooms.js | 17 +++++++----- packages/rocketchat-ui-admin/package.js | 8 +++--- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/rocketchat-ui-admin/client/adminFlex.js b/packages/rocketchat-ui-admin/client/adminFlex.js index 8c5c5da192..c935f2ece7 100644 --- a/packages/rocketchat-ui-admin/client/adminFlex.js +++ b/packages/rocketchat-ui-admin/client/adminFlex.js @@ -37,21 +37,18 @@ Template.adminFlex.helpers({ }; } } - RocketChat.settings.collectionPrivate.find(query).fetch().map((al) => { - return function(el) { - console.log(el); - console.log(al); - el.label = label.apply(el); - return el; - }; - }).sort(() => { - return function(a, b) { - if (a.label.toLowerCase() >= b.label.toLowerCase()) { - return 1; - } else { - return -1; - } - }; + const fetch = RocketChat.settings.collectionPrivate.find(query).fetch(); + console.log(fetch); + + fetch.map((el) => { + el.label = label.apply(el); + return el; + }).sort((a, b) => { + if (a.label.toLowerCase() >= b.label.toLowerCase()) { + return 1; + } else { + return -1; + } }); }, label, diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRooms.js b/packages/rocketchat-ui-admin/client/rooms/adminRooms.js index a566500ec8..6d244bf7b4 100644 --- a/packages/rocketchat-ui-admin/client/rooms/adminRooms.js +++ b/packages/rocketchat-ui-admin/client/rooms/adminRooms.js @@ -1,4 +1,5 @@ -/*globals RocketChatTabBar */ +/*globals RocketChatTabBar, AdminChatRoom */ + this.AdminChatRoom = new Mongo.Collection('rocketchat_room'); Template.adminRooms.helpers({ @@ -22,8 +23,8 @@ Template.adminRooms.helpers({ } }, roomCount() { - const instance = Template.instance(); - return instance.rooms() && instance.rooms().count(); + const rooms = Template.instance().rooms(); + return rooms && rooms.count(); }, name() { if (this.t === 'c' || this.t === 'p') { @@ -94,8 +95,11 @@ Template.adminRooms.onCreated(function() { return instance.ready.set(subscription.ready()); }); this.rooms = function() { - let filter = _.trim((ref = instance.filter) != null ? ref.get() : void 0); - let types = (ref1 = instance.types) != null ? ref1.get() : void 0; + let filter; + if (instance.filter && instance.filter.get()) { + filter = _.trim(instance.filter.get()); + } + let types = instance.types && instance.types.get(); if (!_.isArray(types)) { types = []; } @@ -119,8 +123,9 @@ Template.adminRooms.onCreated(function() { $in: types }; } + const limit = instance.limit && instance.limit.get(); return AdminChatRoom.find(query, { - limit: (ref2 = instance.limit) != null ? ref2.get() : void 0, + limit, sort: { 'default': -1, name: 1 diff --git a/packages/rocketchat-ui-admin/package.js b/packages/rocketchat-ui-admin/package.js index 9fbe0aa75b..abeab32fe3 100644 --- a/packages/rocketchat-ui-admin/package.js +++ b/packages/rocketchat-ui-admin/package.js @@ -39,13 +39,13 @@ Package.onUse(function(api) { // coffee files api.addFiles('client/admin.js', 'client'); - api.addFiles('client/adminFlex.coffee', 'client'); + api.addFiles('client/adminFlex.js', 'client'); api.addFiles('client/adminInfo.js', 'client'); - api.addFiles('client/rooms/adminRooms.coffee', 'client'); + api.addFiles('client/rooms/adminRooms.js', 'client'); - api.addFiles('client/users/adminInviteUser.coffee', 'client'); - api.addFiles('client/users/adminUserChannels.coffee', 'client'); + api.addFiles('client/users/adminInviteUser.js', 'client'); + api.addFiles('client/users/adminUserChannels.js', 'client'); api.addFiles('client/users/adminUsers.js', 'client'); api.addFiles('publications/adminRooms.js', 'server'); -- GitLab From e3155583820e3cad5d8a9205804372d95148bad4 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Fri, 5 May 2017 21:59:45 -0500 Subject: [PATCH 040/280] Allow updating incoming integration PostAs field --- .../client/views/integrationsIncoming.html | 6 +----- .../client/views/integrationsIncoming.js | 3 +-- .../server/methods/incoming/updateIncomingIntegration.js | 5 +++++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.html b/packages/rocketchat-integrations/client/views/integrationsIncoming.html index 68955e653b..1bc5f6e002 100644 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.html +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.html @@ -30,11 +30,7 @@
- {{#if data.token}} - - {{else}} - - {{/if}} +
{{_ "Choose_the_username_that_this_integration_will_post_as"}}
{{_ "Should_exists_a_user_with_this_username"}}
diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.js b/packages/rocketchat-integrations/client/views/integrationsIncoming.js index f255ea62ac..38e4ebfcc7 100644 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.js +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.js @@ -222,6 +222,7 @@ Template.integrationsIncoming.events({ const integration = { enabled: enabled === '1', channel, + username, alias: alias !== '' ? alias : undefined, emoji: emoji !== '' ? emoji : undefined, avatar: avatar !== '' ? avatar : undefined, @@ -240,8 +241,6 @@ Template.integrationsIncoming.events({ toastr.success(TAPi18n.__('Integration_updated')); }); } else { - integration.username = username; - Meteor.call('addIncomingIntegration', integration, (err, data) => { if (err) { return handleError(err); diff --git a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js index 9a4c4a878e..a888484d05 100644 --- a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js +++ b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js @@ -76,6 +76,11 @@ Meteor.methods({ } const user = RocketChat.models.Users.findOne({ username: currentIntegration.username }); + + if (!user || !user._id) { + throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration' }); + } + RocketChat.models.Roles.addUserRoles(user._id, 'bot'); RocketChat.models.Integrations.update(integrationId, { -- GitLab From 8d3852d7161390b13ba21a2a1fb3c015d55dd94f Mon Sep 17 00:00:00 2001 From: Andrew Bromwich Date: Sat, 6 May 2017 21:56:30 +1000 Subject: [PATCH 041/280] Fixed up definition of `isUserFromParams` to handle case where no params are passed => assumption is no user type params to mean 'current user' Refactored `getUserFromParams` to remove duplication of calls to find user --- .../server/v1/helpers/getUserFromParams.js | 29 +++++++++---------- .../server/v1/helpers/isUserFromParams.js | 19 ++++++++++-- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js index 01c075ea0a..f3e4c81950 100644 --- a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js @@ -2,32 +2,29 @@ RocketChat.API.v1.helperMethods.set('getUserFromParams', function _getUserFromParams() { const doesntExist = { _doesntExist: true }; let user; + let params; switch (this.request.method) { case 'POST': case 'PUT': - if (this.bodyParams.userId && this.bodyParams.userId.trim()) { - user = RocketChat.models.Users.findOneById(this.bodyParams.userId) || doesntExist; - } else if (this.bodyParams.username && this.bodyParams.username.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.bodyParams.username) || doesntExist; - } else if (this.bodyParams.user && this.bodyParams.user.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.bodyParams.user) || doesntExist; - } + params = this.bodyParams; break; default: - if (this.queryParams.userId && this.queryParams.userId.trim()) { - user = RocketChat.models.Users.findOneById(this.queryParams.userId) || doesntExist; - } else if (this.queryParams.username && this.queryParams.username.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.queryParams.username) || doesntExist; - } else if (this.queryParams.user && this.queryParams.user.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.queryParams.user) || doesntExist; - } + params = this.queryParams; break; } - if (!user) { + if (params.userId && params.userId.trim()) { + user = RocketChat.models.Users.findOneById(params.userId) || doesntExist; + } else if (params.username && params.username.trim()) { + user = RocketChat.models.Users.findOneByUsername(params.username) || doesntExist; + } else if (params.user && params.user.trim()) { + user = RocketChat.models.Users.findOneByUsername(params.user) || doesntExist; + } else { throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided'); - } else if (user._doesntExist) { + } + + if (user._doesntExist) { throw new Meteor.Error('error-invalid-user', 'The required "userId" or "username" param provided does not match any users'); } diff --git a/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js index f0b24a7809..28c8bf4ee5 100644 --- a/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js @@ -1,5 +1,18 @@ RocketChat.API.v1.helperMethods.set('isUserFromParams', function _isUserFromParams() { - return (this.queryParams.userId && this.userId === this.queryParams.userId) || - (this.queryParams.username && this.user.username === this.queryParams.username) || - (this.queryParams.user && this.user.username === this.queryParams.user); + let params; + + switch (this.request.method) { + case 'POST': + case 'PUT': + params = this.bodyParams; + break; + default: + params = this.queryParams; + break; + } + + return (!params.userId && !params.username && !params.user) || + (params.userId && this.userId === params.userId) || + (params.username && this.user.username === params.username) || + (params.user && this.user.username === params.user); }); -- GitLab From 0dcd164eaec34c87b86f51b261a315c1e70f8c60 Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Mon, 8 May 2017 09:13:25 -0300 Subject: [PATCH 042/280] Allow image insert from slackbridge (closes #5791) --- packages/rocketchat-file-upload/lib/FileUploadBase.js | 4 ++-- packages/rocketchat-slackbridge/slackbridge.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/rocketchat-file-upload/lib/FileUploadBase.js b/packages/rocketchat-file-upload/lib/FileUploadBase.js index 62263dd0c6..24a304cf28 100644 --- a/packages/rocketchat-file-upload/lib/FileUploadBase.js +++ b/packages/rocketchat-file-upload/lib/FileUploadBase.js @@ -2,8 +2,8 @@ /* exported FileUploadBase */ UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({ - insert(userId/*, doc*/) { - return userId; + insert(userId, doc) { + return userId || (doc && doc.message_id && doc.message_id.indexOf('slack-') === 0); // allow inserts from slackbridge (message_id = slack-timestamp-milli) }, update(userId, doc) { return RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', doc.rid) || (RocketChat.settings.get('Message_AllowDeleting') && userId === doc.userId); diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index 2dd320a735..f987bd1d63 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -594,7 +594,6 @@ class SlackBridge { const fileId = Meteor.fileStore.create(details); if (fileId) { Meteor.fileStore.write(stream, fileId, (err, file) => { - console.log('fileStore.write', file); if (err) { throw new Error(err); } else { -- GitLab From f211778d575411de054315c5cb18b71eb9ce04ba Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 8 May 2017 10:40:03 -0300 Subject: [PATCH 043/280] finish the conversion --- packages/rocketchat-ui-admin/client/adminFlex.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/rocketchat-ui-admin/client/adminFlex.js b/packages/rocketchat-ui-admin/client/adminFlex.js index c935f2ece7..874385114f 100644 --- a/packages/rocketchat-ui-admin/client/adminFlex.js +++ b/packages/rocketchat-ui-admin/client/adminFlex.js @@ -1,4 +1,4 @@ -/*globals menu */ +/* globals menu */ Template.adminFlex.onCreated(function() { this.settingsFilter = new ReactiveVar(''); if (RocketChat.settings.cachedCollectionPrivate == null) { @@ -27,7 +27,7 @@ Template.adminFlex.helpers({ let groups = []; records.forEach(function(record) { if (filterRegex.test(TAPi18n.__(record.i18nLabel || record._id))) { - groups.push(record.group || record._id); + return groups.push(record.group || record._id); } }); groups = _.unique(groups); @@ -37,13 +37,10 @@ Template.adminFlex.helpers({ }; } } - const fetch = RocketChat.settings.collectionPrivate.find(query).fetch(); - console.log(fetch); - - fetch.map((el) => { + return RocketChat.settings.collectionPrivate.find(query).fetch().map(function(el) { el.label = label.apply(el); return el; - }).sort((a, b) => { + }).sort(function(a, b) { if (a.label.toLowerCase() >= b.label.toLowerCase()) { return 1; } else { -- GitLab From e39762113ccbae2744fb6a0c6684cdf4ecda99e5 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 8 May 2017 10:44:40 -0300 Subject: [PATCH 044/280] remove coffee files --- .../rocketchat-ui-admin/client/admin.coffee | 488 ------------------ .../client/adminFlex.coffee | 62 --- .../client/adminInfo.coffee | 64 --- .../client/rooms/adminRoomInfo.coffee | 186 ------- .../client/rooms/adminRooms.coffee | 126 ----- .../client/users/adminInviteUser.coffee | 31 -- .../client/users/adminUserChannels.coffee | 11 - .../client/users/adminUsers.coffee | 108 ---- 8 files changed, 1076 deletions(-) delete mode 100644 packages/rocketchat-ui-admin/client/admin.coffee delete mode 100644 packages/rocketchat-ui-admin/client/adminFlex.coffee delete mode 100644 packages/rocketchat-ui-admin/client/adminInfo.coffee delete mode 100644 packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee delete mode 100644 packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee delete mode 100644 packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee delete mode 100644 packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee delete mode 100644 packages/rocketchat-ui-admin/client/users/adminUsers.coffee diff --git a/packages/rocketchat-ui-admin/client/admin.coffee b/packages/rocketchat-ui-admin/client/admin.coffee deleted file mode 100644 index 8e8fe21469..0000000000 --- a/packages/rocketchat-ui-admin/client/admin.coffee +++ /dev/null @@ -1,488 +0,0 @@ -import toastr from 'toastr' -TempSettings = new Mongo.Collection null -RocketChat.TempSettings = TempSettings - -getDefaultSetting = (settingId) -> - return RocketChat.settings.collectionPrivate.findOne({_id: settingId}) - -setFieldValue = (settingId, value, type, editor) -> - input = $('.page-settings').find('[name="' + settingId + '"]') - - switch type - when 'boolean' - $('.page-settings').find('[name="' + settingId + '"][value="' + Number(value) + '"]').prop('checked', true).change() - when 'code' - input.next()[0].CodeMirror.setValue(value) - when 'color' - input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change() - input.val(value).change() - - if editor is 'color' - new jscolor(input) - - else - input.val(value).change() - -Template.admin.onCreated -> - if not RocketChat.settings.cachedCollectionPrivate? - RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ name: 'private-settings', eventType: 'onLogged' }) - RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection - RocketChat.settings.cachedCollectionPrivate.init() - - this.selectedRooms = new ReactiveVar {} - - RocketChat.settings.collectionPrivate.find().observe - added: (data) => - selectedRooms = this.selectedRooms.get() - if data.type is 'roomPick' - selectedRooms[data._id] = data.value - this.selectedRooms.set(selectedRooms) - TempSettings.insert data - changed: (data) => - selectedRooms = this.selectedRooms.get() - if data.type is 'roomPick' - selectedRooms[data._id] = data.value - this.selectedRooms.set(selectedRooms) - TempSettings.update data._id, data - removed: (data) => - selectedRooms = this.selectedRooms.get() - if data.type is 'roomPick' - delete selectedRooms[data._id] - this.selectedRooms.set(selectedRooms) - TempSettings.remove data._id - -Template.admin.onDestroyed -> - TempSettings.remove {} - -Template.admin.helpers - languages: -> - languages = TAPi18n.getLanguages() - result = [] - for key, language of languages - result.push _.extend(language, { key: key }) - result = _.sortBy(result, 'key') - result.unshift { - "name": "Default", - "en": "Default", - "key": "" - } - return result; - - appLanguage: (key) -> - return (RocketChat.settings.get('Language'))?.split('-').shift().toLowerCase() is key - - group: -> - groupId = FlowRouter.getParam('group') - group = RocketChat.settings.collectionPrivate.findOne { _id: groupId, type: 'group' } - - if not group - return - - settings = RocketChat.settings.collectionPrivate.find({ group: groupId }, {sort: {section: 1, sorter: 1, i18nLabel: 1}}).fetch() - - sections = {} - for setting in settings - if setting.i18nDefaultQuery? - if _.isString(setting.i18nDefaultQuery) - i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery) - else - i18nDefaultQuery = setting.i18nDefaultQuery - - if not _.isArray(i18nDefaultQuery) - i18nDefaultQuery = [i18nDefaultQuery] - - found = 0 - for item in i18nDefaultQuery - if RocketChat.settings.collectionPrivate.findOne(item)? - setting.value = TAPi18n.__(setting._id + '_Default') - - sections[setting.section or ''] ?= [] - sections[setting.section or ''].push setting - - group.sections = [] - for key, value of sections - group.sections.push - section: key - settings: value - - return group - - i18nDefaultValue: -> - return TAPi18n.__(@_id + '_Default') - - isDisabled: -> - if @blocked - return { disabled: 'disabled' } - - if not @enableQuery? - return {} - - if _.isString(@enableQuery) - enableQuery = JSON.parse(@enableQuery) - else - enableQuery = @enableQuery - - if not _.isArray(enableQuery) - enableQuery = [enableQuery] - - found = 0 - console.log enableQuery - for item in enableQuery - if TempSettings.findOne(item)? - found++ - - return if found is enableQuery.length then {} else {disabled: 'disabled'} - - isReadonly: -> - if @readonly is true - return { readonly: 'readonly' } - - hasChanges: (section) -> - group = FlowRouter.getParam('group') - - query = - group: group - changed: true - - if section? - if section is '' - query.$or = [ - {section: ''} - {section: {$exists: false}} - ] - else - query.section = section - - return TempSettings.find(query).count() > 0 - - isSettingChanged: (id) -> - return TempSettings.findOne({_id: id}, {fields: {changed: 1}}).changed - - translateSection: (section) -> - if section.indexOf(':') > -1 - return section - - return t(section) - - label: -> - label = @i18nLabel or @_id - return TAPi18n.__ label if label - - description: -> - description = TAPi18n.__ @i18nDescription if @i18nDescription - if description? and description isnt @i18nDescription - return description - - sectionIsCustomOAuth: (section) -> - return /^Custom OAuth:\s.+/.test section - - callbackURL: (section) -> - id = s.strRight(section, 'Custom OAuth: ').toLowerCase() - return Meteor.absoluteUrl('_oauth/' + id) - - relativeUrl: (url) -> - return Meteor.absoluteUrl(url) - - selectedOption: (_id, val) -> - return RocketChat.settings.collectionPrivate.findOne({_id: _id})?.value is val - - random: -> - return Random.id() - - getEditorOptions: (readOnly = false) -> - return {} = - lineNumbers: true - mode: this.code or "javascript" - gutters: [ - "CodeMirror-linenumbers" - "CodeMirror-foldgutter" - ] - foldGutter: true - matchBrackets: true - autoCloseBrackets: true - matchTags: true, - showTrailingSpace: true - highlightSelectionMatches: true - readOnly: readOnly - - setEditorOnBlur: (_id) -> - Meteor.defer -> - return if not $('.code-mirror-box[data-editor-id="'+_id+'"] .CodeMirror')[0] - - codeMirror = $('.code-mirror-box[data-editor-id="'+_id+'"] .CodeMirror')[0].CodeMirror - if codeMirror.changeAdded is true - return - - onChange = -> - value = codeMirror.getValue() - TempSettings.update {_id: _id}, - $set: - value: value - changed: RocketChat.settings.collectionPrivate.findOne(_id).value isnt value - - onChangeDelayed = _.debounce onChange, 500 - - codeMirror.on 'change', onChangeDelayed - codeMirror.changeAdded = true - - return - - assetAccept: (fileConstraints) -> - if fileConstraints.extensions?.length > 0 - return '.' + fileConstraints.extensions.join(', .') - - autocompleteRoom: -> - return { - limit: 10 - # inputDelay: 300 - rules: [ - { - # @TODO maybe change this 'collection' and/or template - collection: 'CachedChannelList' - subscription: 'channelAndPrivateAutocomplete' - field: 'name' - template: Template.roomSearch - noMatchTemplate: Template.roomSearchEmpty - matchAll: true - selector: (match) -> - return { name: match } - sort: 'name' - } - ] - } - - selectedRooms: -> - return Template.instance().selectedRooms.get()[this._id] or [] - - getColorVariable: (color) -> - return color.replace(/theme-color-/, '@') - - showResetButton: -> - setting = TempSettings.findOne({ _id: @_id }, { fields: { value: 1, packageValue: 1 } }) - return @type isnt 'asset' and setting.value isnt setting.packageValue and not @blocked - -Template.admin.events - "change .input-monitor, keyup .input-monitor": _.throttle((e, t) -> - value = _.trim $(e.target).val() - - switch @type - when 'int' - value = parseInt(value) - when 'boolean' - value = value is "1" - - TempSettings.update {_id: @_id}, - $set: - value: value - changed: RocketChat.settings.collectionPrivate.findOne(@_id).value isnt value - , 500) - - "change select[name=color-editor]": (e, t) -> - value = _.trim $(e.target).val() - TempSettings.update {_id: @_id}, - $set: - editor: value - - "click .submit .discard": -> - group = FlowRouter.getParam('group') - - query = - group: group - changed: true - - settings = TempSettings.find(query, {fields: {_id: 1, value: 1, packageValue: 1}}).fetch() - - settings.forEach (setting) -> - oldSetting = RocketChat.settings.collectionPrivate.findOne({_id: setting._id}, {fields: {value: 1, type:1, editor: 1}}) - - setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor) - - "click .reset-setting": (e, t) -> - e.preventDefault(); - settingId = $(e.target).data('setting') - if typeof settingId is 'undefined' then settingId = $(e.target).parent().data('setting') - - defaultValue = getDefaultSetting(settingId) - - setFieldValue(settingId, defaultValue.packageValue, defaultValue.type, defaultValue.editor) - - "click .reset-group": (e, t) -> - e.preventDefault(); - group = FlowRouter.getParam('group') - section = $(e.target).data('section') - - if section is "" - settings = TempSettings.find({group: group, section: {$exists: false}}, {fields: {_id: 1}}).fetch() - else - settings = TempSettings.find({group: group, section: section}, {fields: {_id: 1}}).fetch() - - settings.forEach (setting) -> - defaultValue = getDefaultSetting(setting._id) - setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor) - - TempSettings.update {_id: setting._id}, - $set: - value: defaultValue.packageValue - changed: RocketChat.settings.collectionPrivate.findOne(setting._id).value isnt defaultValue.packageValue - - "click .submit .save": (e, t) -> - group = FlowRouter.getParam('group') - - query = - group: group - changed: true - - settings = TempSettings.find(query, {fields: {_id: 1, value: 1, editor: 1}}).fetch() - - if not _.isEmpty settings - RocketChat.settings.batchSet settings, (err, success) -> - return handleError(err) if err - TempSettings.update({changed: true}, {$unset: {changed: 1}}) - toastr.success TAPi18n.__ 'Settings_updated' - - "click .submit .refresh-clients": (e, t) -> - Meteor.call 'refreshClients', -> - toastr.success TAPi18n.__ 'Clients_will_refresh_in_a_few_seconds' - - "click .submit .add-custom-oauth": (e, t) -> - config = - title: TAPi18n.__ 'Add_custom_oauth' - text: TAPi18n.__ 'Give_a_unique_name_for_the_custom_oauth' - type: "input", - showCancelButton: true, - closeOnConfirm: true, - inputPlaceholder: TAPi18n.__ 'Custom_oauth_unique_name' - - swal config, (inputValue) -> - if inputValue is false - return false - - if inputValue is "" - swal.showInputError TAPi18n.__ 'Name_cant_be_empty' - return false - - Meteor.call 'addOAuthService', inputValue, (err) -> - if err - handleError(err) - - "click .submit .refresh-oauth": (e, t) -> - toastr.info TAPi18n.__ 'Refreshing' - Meteor.call 'refreshOAuthService', (err) -> - if err - handleError(err) - else - toastr.success TAPi18n.__ 'Done' - - "click .submit .remove-custom-oauth": (e, t) -> - name = this.section.replace('Custom OAuth: ', '') - config = - title: TAPi18n.__ 'Are_you_sure' - type: "input", - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: TAPi18n.__ 'Yes_delete_it' - cancelButtonText: TAPi18n.__ 'Cancel' - closeOnConfirm: true - - swal config, -> - Meteor.call 'removeOAuthService', name - - "click .delete-asset": -> - Meteor.call 'unsetAsset', @asset - - "change input[type=file]": (ev) -> - e = ev.originalEvent or ev - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - for blob in files - toastr.info TAPi18n.__ 'Uploading_file' - - # if @fileConstraints.contentType isnt blob.type - # toastr.error blob.type, TAPi18n.__ 'Invalid_file_type' - # return - - reader = new FileReader() - reader.readAsBinaryString(blob) - reader.onloadend = => - Meteor.call 'setAsset', reader.result, blob.type, @asset, (err, data) -> - if err? - handleError(err) - # toastr.error err.reason, TAPi18n.__ err.error - console.log err - return - - toastr.success TAPi18n.__ 'File_uploaded' - - "click .expand": (e) -> - $(e.currentTarget).closest('.section').removeClass('section-collapsed') - $(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__ "Collapse") - $('.CodeMirror').each (index, codeMirror) -> - codeMirror.CodeMirror.refresh() - - "click .collapse": (e) -> - $(e.currentTarget).closest('.section').addClass('section-collapsed') - $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__ "Expand") - - "click button.action": (e) -> - if @type isnt 'action' - return - - Meteor.call @value, (err, data) -> - if err? - err.details = _.extend(err.details || {}, errorTitle: 'Error') - handleError(err) - return - - args = [data.message].concat data.params - - toastr.success TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success') - - "click .button-fullscreen": -> - codeMirrorBox = $('.code-mirror-box[data-editor-id="'+this._id+'"]') - codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color') - codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh() - - "click .button-restore": -> - codeMirrorBox = $('.code-mirror-box[data-editor-id="'+this._id+'"]') - codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color') - codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh() - - 'autocompleteselect .autocomplete': (event, instance, doc) -> - selectedRooms = instance.selectedRooms.get() - selectedRooms[this.id] = (selectedRooms[this.id] || []).concat doc - instance.selectedRooms.set selectedRooms - value = selectedRooms[this.id] - TempSettings.update {_id: this.id}, - $set: - value: value - changed: RocketChat.settings.collectionPrivate.findOne(this.id).value isnt value - event.currentTarget.value = '' - event.currentTarget.focus() - - 'click .remove-room': (event, instance) -> - docId = this._id - settingId = event.currentTarget.getAttribute('data-setting') - selectedRooms = instance.selectedRooms.get() - selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], (setting) -> setting._id is docId) - instance.selectedRooms.set selectedRooms - value = selectedRooms[settingId] - TempSettings.update {_id: settingId}, - $set: - value: value - changed: RocketChat.settings.collectionPrivate.findOne(settingId).value isnt value - -Template.admin.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - - Tracker.autorun -> - hasColor = TempSettings.findOne { group: FlowRouter.getParam('group'), type: 'color' }, { fields: { _id: 1 } } - if hasColor - Meteor.setTimeout -> - $('.colorpicker-input').each (index, el) -> - new jscolor(el) - , 400 diff --git a/packages/rocketchat-ui-admin/client/adminFlex.coffee b/packages/rocketchat-ui-admin/client/adminFlex.coffee deleted file mode 100644 index a7f30f2acf..0000000000 --- a/packages/rocketchat-ui-admin/client/adminFlex.coffee +++ /dev/null @@ -1,62 +0,0 @@ -Template.adminFlex.onCreated -> - @settingsFilter = new ReactiveVar('') - - if not RocketChat.settings.cachedCollectionPrivate? - RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ name: 'private-settings', eventType: 'onLogged' }) - RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection - RocketChat.settings.cachedCollectionPrivate.init() - -label = -> - return TAPi18n.__(@i18nLabel or @_id) - -Template.adminFlex.helpers - groups: -> - filter = Template.instance().settingsFilter.get() - - query = - type: 'group' - - if filter - filterRegex = new RegExp(_.escapeRegExp(filter), 'i') - - records = RocketChat.settings.collectionPrivate.find().fetch() - groups = [] - records = records.forEach (record) -> - if filterRegex.test(TAPi18n.__(record.i18nLabel or record._id)) - groups.push(record.group or record._id) - - groups = _.unique(groups) - if groups.length > 0 - query._id = - $in: groups - RocketChat.settings.collectionPrivate.find(query).fetch() - .map (el) => - el.label = label.apply(el) - return el - .sort (a, b) => - if a.label.toLowerCase() >= b.label.toLowerCase() then 1 else -1 - - label: label - - adminBoxOptions: -> - return RocketChat.AdminBox.getOptions() - - -Template.adminFlex.events - 'mouseenter header': -> - SideNav.overArrow() - - 'mouseleave header': -> - SideNav.leaveArrow() - - 'click header': -> - SideNav.closeFlex() - - 'click .cancel-settings': -> - SideNav.closeFlex() - - 'click .admin-link': -> - menu.close() - - 'keyup [name=settings-search]': (e, t) -> - t.settingsFilter.set(e.target.value) diff --git a/packages/rocketchat-ui-admin/client/adminInfo.coffee b/packages/rocketchat-ui-admin/client/adminInfo.coffee deleted file mode 100644 index 51cea01511..0000000000 --- a/packages/rocketchat-ui-admin/client/adminInfo.coffee +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment' - -Template.adminInfo.helpers - isReady: -> - return Template.instance().ready.get() - statistics: -> - return Template.instance().statistics.get() - inGB: (size) -> - if size > 1073741824 - return _.numberFormat(size / 1024 / 1024 / 1024, 2) + ' GB' - return _.numberFormat(size / 1024 / 1024, 2) + ' MB' - humanReadableTime: (time) -> - days = Math.floor time / 86400 - hours = Math.floor (time % 86400) / 3600 - minutes = Math.floor ((time % 86400) % 3600) / 60 - seconds = Math.floor ((time % 86400) % 3600) % 60 - out = "" - if days > 0 - out += "#{days} #{TAPi18n.__ 'days'}, " - if hours > 0 - out += "#{hours} #{TAPi18n.__ 'hours'}, " - if minutes > 0 - out += "#{minutes} #{TAPi18n.__ 'minutes'}, " - if seconds > 0 - out += "#{seconds} #{TAPi18n.__ 'seconds'}" - return out - formatDate: (date) -> - if date - return moment(date).format("LLL") - numFormat: (number) -> - return _.numberFormat(number, 2) - info: -> - return RocketChat.Info - build: -> - return RocketChat.Info?.compile || RocketChat.Info?.build - -Template.adminInfo.events - 'click .refresh': (e, instance) -> - instance.ready.set false - Meteor.call 'getStatistics', true, (error, statistics) -> - instance.ready.set true - if error - handleError(error) - else - instance.statistics.set statistics - -Template.adminInfo.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - -Template.adminInfo.onCreated -> - instance = @ - @statistics = 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 - diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee b/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee deleted file mode 100644 index 9667b35668..0000000000 --- a/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee +++ /dev/null @@ -1,186 +0,0 @@ -import toastr from 'toastr' -Template.adminRoomInfo.helpers - selectedRoom: -> - return Session.get 'adminRoomsSelected' - canEdit: -> - return RocketChat.authz.hasAllPermission('edit-room', @rid) - editing: (field) -> - return Template.instance().editing.get() is field - notDirect: -> - return AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t isnt 'd' - roomType: -> - return AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - channelSettings: -> - return RocketChat.ChannelSettings.getOptions(null, 'admin-room') - roomTypeDescription: -> - roomType = AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - if roomType is 'c' - return t('Channel') - else if roomType is 'p' - return t('Private_Group') - roomName: -> - return AdminChatRoom.findOne(@rid, { fields: { name: 1 }})?.name - roomTopic: -> - return AdminChatRoom.findOne(@rid, { fields: { topic: 1 }})?.topic - archivationState: -> - return AdminChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived - archivationStateDescription: -> - archivationState = AdminChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived - if archivationState is true - return t('Room_archivation_state_true') - else - return t('Room_archivation_state_false') - canDeleteRoom: -> - roomType = AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - return roomType? and RocketChat.authz.hasAtLeastOnePermission("delete-#{roomType}") - readOnly: -> - room = AdminChatRoom.findOne(@rid, { fields: { ro: 1 }}) - return room?.ro - readOnlyDescription: -> - room = AdminChatRoom.findOne(@rid, { fields: { ro: 1 }}) - readOnly = room?.ro - if readOnly is true - return t('True') - else - return t('False') - -Template.adminRoomInfo.events - 'click .delete': -> - swal { - title: t('Are_you_sure') - text: t('Delete_Room_Warning') - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: t('Yes_delete_it') - cancelButtonText: t('Cancel') - closeOnConfirm: false - html: false - }, => - swal.disableButtons() - - Meteor.call 'eraseRoom', @rid, (error, result) -> - if error - handleError(error) - swal.enableButtons() - else - swal - title: t('Deleted') - text: t('Room_has_been_deleted') - type: 'success' - timer: 2000 - showConfirmButton: false - - 'keydown input[type=text]': (e, t) -> - if e.keyCode is 13 - e.preventDefault() - t.saveSetting(@rid) - - 'click [data-edit]': (e, t) -> - e.preventDefault() - t.editing.set($(e.currentTarget).data('edit')) - setTimeout (-> t.$('input.editing').focus().select()), 100 - - 'click .cancel': (e, t) -> - e.preventDefault() - t.editing.set() - - 'click .save': (e, t) -> - e.preventDefault() - t.saveSetting(@rid) - -Template.adminRoomInfo.onCreated -> - @editing = new ReactiveVar - - @validateRoomType = (rid) => - type = @$('input[name=roomType]:checked').val() - if type not in ['c', 'p'] - toastr.error t('error-invalid-room-type', { type: type }) - return true - - @validateRoomName = (rid) => - room = AdminChatRoom.findOne rid - - if not RocketChat.authz.hasAllPermission('edit-room', rid) or room.t not in ['c', 'p'] - toastr.error t('error-not-allowed') - return false - - name = $('input[name=roomName]').val() - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test name - toastr.error t('error-invalid-room-name', { room_name: name }) - return false - - return true - - @validateRoomTopic = (rid) => - return true - - @saveSetting = (rid) => - switch @editing.get() - when 'roomName' - if @validateRoomName(rid) - RocketChat.callbacks.run 'roomNameChanged', AdminChatRoom.findOne(rid) - Meteor.call 'saveRoomSettings', rid, 'roomName', @$('input[name=roomName]').val(), (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_name_changed_successfully' - when 'roomTopic' - if @validateRoomTopic(rid) - Meteor.call 'saveRoomSettings', rid, 'roomTopic', @$('input[name=roomTopic]').val(), (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_topic_changed_successfully' - RocketChat.callbacks.run 'roomTopicChanged', AdminChatRoom.findOne(rid) - when 'roomAnnouncement' - if @validateRoomTopic(rid) - Meteor.call 'saveRoomSettings', rid, 'roomAnnouncement', @$('input[name=roomAnnouncement]').val(), (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_announcement_changed_successfully' - RocketChat.callbacks.run 'roomAnnouncementChanged', AdminChatRoom.findOne(rid) - when 'roomType' - val = @$('input[name=roomType]:checked').val() - if @validateRoomType(rid) - RocketChat.callbacks.run 'roomTypeChanged', AdminChatRoom.findOne(rid) - saveRoomSettings = => - Meteor.call 'saveRoomSettings', rid, 'roomType', val, (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_type_changed_successfully' - unless AdminChatRoom.findOne(rid, { fields: { default: 1 }}).default - return saveRoomSettings() - swal - title: t('Room_default_change_to_private_will_be_default_no_more') - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: t('Yes') - cancelButtonText: t('Cancel') - closeOnConfirm: true - html: false - (confirmed) => - return !confirmed || saveRoomSettings() - when 'archivationState' - if @$('input[name=archivationState]:checked').val() is 'true' - if AdminChatRoom.findOne(rid)?.archived isnt true - Meteor.call 'archiveRoom', rid, (err, results) -> - return handleError(err) if err - toastr.success TAPi18n.__ 'Room_archived' - RocketChat.callbacks.run 'archiveRoom', AdminChatRoom.findOne(rid) - else - if AdminChatRoom.findOne(rid)?.archived is true - Meteor.call 'unarchiveRoom', rid, (err, results) -> - return handleError(err) if err - toastr.success TAPi18n.__ 'Room_unarchived' - RocketChat.callbacks.run 'unarchiveRoom', AdminChatRoom.findOne(rid) - when 'readOnly' - Meteor.call 'saveRoomSettings', rid, 'readOnly', @$('input[name=readOnly]:checked').val() is 'true', (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Read_only_changed_successfully' - @editing.set() diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee b/packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee deleted file mode 100644 index 1be4c06b44..0000000000 --- a/packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee +++ /dev/null @@ -1,126 +0,0 @@ -@AdminChatRoom = new Mongo.Collection('rocketchat_room') - -Template.adminRooms.helpers - isReady: -> - return Template.instance().ready?.get() - rooms: -> - return Template.instance().rooms() - isLoading: -> - return 'btn-loading' unless Template.instance().ready?.get() - hasMore: -> - return Template.instance().limit?.get() is Template.instance().rooms?().count() - roomCount: -> - return Template.instance().rooms?().count() - name: -> - if @t is 'c' or @t is 'p' - return @name - else if @t is 'd' - return @usernames.join ' x ' - type: -> - if @t is 'c' - return TAPi18n.__ 'Channel' - else if @t is 'd' - return TAPi18n.__ 'Direct Message' - if @t is 'p' - return TAPi18n.__ 'Private Group' - default: -> - if this.default - return t('True') - else - return t('False') - flexData: -> - return { - tabBar: Template.instance().tabBar - } - -Template.adminRooms.onCreated -> - instance = @ - @limit = new ReactiveVar 50 - @filter = new ReactiveVar '' - @types = new ReactiveVar [] - @ready = new ReactiveVar true - - @tabBar = new RocketChatTabBar(); - @tabBar.showGroup(FlowRouter.current().route.name); - - RocketChat.TabBar.addButton({ - groups: ['admin-rooms'], - id: 'admin-room', - i18nTitle: 'Room_Info', - icon: 'icon-info-circled', - template: 'adminRoomInfo', - order: 1 - }); - - RocketChat.ChannelSettings.addOption - group: ['admin-room'] - id: 'make-default' - template: 'channelSettingsDefault' - data: -> - return Session.get('adminRoomsSelected') - validation: -> - return RocketChat.authz.hasAllPermission('view-room-administration') - - @autorun -> - filter = instance.filter.get() - types = instance.types.get() - - if types.length is 0 - types = ['c', 'd', 'p'] - - limit = instance.limit.get() - subscription = instance.subscribe 'adminRooms', filter, types, limit - instance.ready.set subscription.ready() - - @rooms = -> - filter = _.trim instance.filter?.get() - types = instance.types?.get() - - unless _.isArray types - types = [] - - query = {} - - filter = _.trim filter - if filter - filterReg = new RegExp s.escapeRegExp(filter), "i" - query = { $or: [ { name: filterReg }, { t: 'd', usernames: filterReg } ] } - - if types.length - query['t'] = { $in: types } - - return AdminChatRoom.find(query, { limit: instance.limit?.get(), sort: { default: -1, name: 1 } }) - - @getSearchTypes = -> - return _.map $('[name=room-type]:checked'), (input) -> return $(input).val() - -Template.adminRooms.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - -Template.adminRooms.events - 'keydown #rooms-filter': (e) -> - if e.which is 13 - e.stopPropagation() - e.preventDefault() - - 'keyup #rooms-filter': (e, t) -> - e.stopPropagation() - e.preventDefault() - t.filter.set e.currentTarget.value - - 'click .room-info': (e, instance) -> - e.preventDefault() - - Session.set('adminRoomsSelected', { rid: @_id }); - - instance.tabBar.open('admin-room') - - 'click .load-more': (e, t) -> - e.preventDefault() - e.stopPropagation() - t.limit.set t.limit.get() + 50 - - 'change [name=room-type]': (e, t) -> - t.types.set t.getSearchTypes() diff --git a/packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee b/packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee deleted file mode 100644 index f58c990149..0000000000 --- a/packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee +++ /dev/null @@ -1,31 +0,0 @@ -import toastr from 'toastr' -Template.adminInviteUser.helpers - isAdmin: -> - return RocketChat.authz.hasRole(Meteor.userId(), 'admin') - inviteEmails: -> - return Template.instance().inviteEmails.get() - -Template.adminInviteUser.events - 'click .send': (e, instance) -> - emails = $('#inviteEmails').val().split /[\s,;]/ - rfcMailPattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ - validEmails = _.compact _.map emails, (email) -> return email if rfcMailPattern.test email - if validEmails.length - Meteor.call 'sendInvitationEmail', validEmails, (error, result) -> - if result - instance.clearForm() - instance.inviteEmails.set validEmails - if error - handleError(error) - else - toastr.error t('Send_invitation_email_error') - - 'click .cancel': (e, instance) -> - instance.clearForm() - instance.inviteEmails.set [] - Template.currentData().tabBar.close() - -Template.adminInviteUser.onCreated -> - @inviteEmails = new ReactiveVar [] - @clearForm = -> - $('#inviteEmails').val('') diff --git a/packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee b/packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee deleted file mode 100644 index f7f5520e92..0000000000 --- a/packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee +++ /dev/null @@ -1,11 +0,0 @@ -Template.adminUserChannels.helpers - type: -> - return if @t is 'd' then 'at' else if @t is 'p' then 'lock' else 'hash' - route: -> - return switch @t - when 'd' - FlowRouter.path('direct', {username: @name}) - when 'p' - FlowRouter.path('group', {name: @name}) - when 'c' - FlowRouter.path('channel', {name: @name}) diff --git a/packages/rocketchat-ui-admin/client/users/adminUsers.coffee b/packages/rocketchat-ui-admin/client/users/adminUsers.coffee deleted file mode 100644 index ad8e36d320..0000000000 --- a/packages/rocketchat-ui-admin/client/users/adminUsers.coffee +++ /dev/null @@ -1,108 +0,0 @@ -Template.adminUsers.helpers - isReady: -> - return Template.instance().ready?.get() - users: -> - return Template.instance().users() - isLoading: -> - return 'btn-loading' unless Template.instance().ready?.get() - hasMore: -> - return Template.instance().limit?.get() is Template.instance().users?().length - emailAddress: -> - return _.map(@emails, (e) -> e.address).join(', ') - flexData: -> - return { - tabBar: Template.instance().tabBar - data: Template.instance().tabBarData.get() - } - -Template.adminUsers.onCreated -> - instance = @ - @limit = new ReactiveVar 50 - @filter = new ReactiveVar '' - @ready = new ReactiveVar true - - @tabBar = new RocketChatTabBar(); - @tabBar.showGroup(FlowRouter.current().route.name); - - @tabBarData = new ReactiveVar - - RocketChat.TabBar.addButton({ - groups: ['admin-users'], - id: 'invite-user', - i18nTitle: 'Invite_Users', - icon: 'icon-paper-plane', - template: 'adminInviteUser', - order: 1 - }) - - RocketChat.TabBar.addButton({ - groups: ['admin-users'], - id: 'add-user', - i18nTitle: 'Add_User', - icon: 'icon-plus', - template: 'adminUserEdit', - order: 2 - }) - - RocketChat.TabBar.addButton({ - groups: ['admin-users'] - id: 'admin-user-info', - i18nTitle: 'User_Info', - icon: 'icon-user', - template: 'adminUserInfo', - order: 3 - }) - - @autorun -> - filter = instance.filter.get() - limit = instance.limit.get() - subscription = instance.subscribe 'fullUserData', filter, limit - instance.ready.set subscription.ready() - - @users = -> - filter = _.trim instance.filter?.get() - if filter - filterReg = new RegExp s.escapeRegExp(filter), "i" - query = { $or: [ { username: filterReg }, { name: filterReg }, { "emails.address": filterReg } ] } - else - query = {} - - query.type = - $in: ['user', 'bot'] - - return Meteor.users.find(query, { limit: instance.limit?.get(), sort: { username: 1, name: 1 } }).fetch() - -Template.adminUsers.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - -Template.adminUsers.events - 'keydown #users-filter': (e) -> - if e.which is 13 - e.stopPropagation() - e.preventDefault() - - 'keyup #users-filter': (e, t) -> - e.stopPropagation() - e.preventDefault() - t.filter.set e.currentTarget.value - - 'click .user-info': (e, instance) -> - e.preventDefault() - - instance.tabBarData.set Meteor.users.findOne @_id - instance.tabBar.open('admin-user-info') - - 'click .info-tabs button': (e) -> - e.preventDefault() - $('.info-tabs button').removeClass 'active' - $(e.currentTarget).addClass 'active' - - $('.user-info-content').hide() - $($(e.currentTarget).attr('href')).show() - - 'click .load-more': (e, t) -> - e.preventDefault() - e.stopPropagation() - t.limit.set t.limit.get() + 50 -- GitLab From 58bb172ad313f9df0c1025aaf6e510c6c3c84395 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 8 May 2017 11:57:41 -0300 Subject: [PATCH 045/280] finished rocket-chat-ui --- .../client/lib/RoomHistoryManager.coffee | 219 ------------- .../client/lib/RoomHistoryManager.js | 266 +++++++++++++++ .../client/lib/RoomManager.coffee | 253 --------------- .../rocketchat-ui/client/lib/RoomManager.js | 302 ++++++++++++++++++ .../rocketchat-ui/client/lib/readMessages.js | 2 +- .../rocketchat-ui/client/lib/rocket.coffee | 112 ------- packages/rocketchat-ui/client/lib/rocket.js | 121 +++++++ .../rocketchat-ui/client/views/app/room.js | 2 +- packages/rocketchat-ui/package.js | 8 +- 9 files changed, 695 insertions(+), 590 deletions(-) delete mode 100644 packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee create mode 100644 packages/rocketchat-ui/client/lib/RoomHistoryManager.js delete mode 100644 packages/rocketchat-ui/client/lib/RoomManager.coffee create mode 100644 packages/rocketchat-ui/client/lib/RoomManager.js delete mode 100644 packages/rocketchat-ui/client/lib/rocket.coffee create mode 100644 packages/rocketchat-ui/client/lib/rocket.js diff --git a/packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee b/packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee deleted file mode 100644 index 7afe45fdd8..0000000000 --- a/packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee +++ /dev/null @@ -1,219 +0,0 @@ -@RoomHistoryManager = new class - defaultLimit = 50 - - histories = {} - - getRoom = (rid) -> - if not histories[rid]? - histories[rid] = - hasMore: new ReactiveVar true - hasMoreNext: new ReactiveVar false - isLoading: new ReactiveVar false - unreadNotLoaded: new ReactiveVar 0 - firstUnread: new ReactiveVar - loaded: undefined - - return histories[rid] - - getMore = (rid, limit=defaultLimit) -> - room = getRoom rid - if room.hasMore.curValue isnt true - return - - room.isLoading.set true - - # ScrollListener.setLoader true - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) - # lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) - - if lastMessage? - ts = lastMessage.ts - else - ts = undefined - - ls = undefined - typeName = undefined - - subscription = ChatSubscription.findOne rid: rid - if subscription? - ls = subscription.ls - typeName = subscription.t + subscription.name - else - curRoomDoc = ChatRoom.findOne(_id: rid) - typeName = curRoomDoc?.t + curRoomDoc?.name - - Meteor.call 'loadHistory', rid, ts, limit, ls, (err, result) -> - room.unreadNotLoaded.set result?.unreadNotLoaded - room.firstUnread.set result?.firstUnread - - wrapper = $('.messages-box .wrapper').get(0) - if wrapper? - previousHeight = wrapper.scrollHeight - - for item in result?.messages or [] when item.t isnt 'command' - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - - if wrapper? - heightDiff = wrapper.scrollHeight - previousHeight - wrapper.scrollTop += heightDiff - - Meteor.defer -> - readMessage.refreshUnreadMark(rid, true) - RoomManager.updateMentionsMarksOfRoom typeName - - room.isLoading.set false - room.loaded ?= 0 - if result?.messages?.length? - room.loaded += result.messages.length - if result?.messages?.length < limit - room.hasMore.set false - - getMoreNext = (rid, limit=defaultLimit) -> - room = getRoom rid - if room.hasMoreNext.curValue isnt true - return - - instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance() - instance.atBottom = false - - room.isLoading.set true - - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: -1}}) - - typeName = undefined - - subscription = ChatSubscription.findOne rid: rid - if subscription? - ls = subscription.ls - typeName = subscription.t + subscription.name - else - curRoomDoc = ChatRoom.findOne(_id: rid) - typeName = curRoomDoc?.t + curRoomDoc?.name - - ts = lastMessage.ts - - if ts - Meteor.call 'loadNextMessages', rid, ts, limit, (err, result) -> - for item in result?.messages or [] - if item.t isnt 'command' - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - - Meteor.defer -> - RoomManager.updateMentionsMarksOfRoom typeName - - room.isLoading.set false - room.loaded ?= 0 - if result.messages.length? - room.loaded += result.messages.length - if result.messages.length < limit - room.hasMoreNext.set false - - getSurroundingMessages = (message, limit=defaultLimit) -> - unless message?.rid - return - - instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance() - - if ChatMessage.findOne message._id - wrapper = $('.messages-box .wrapper') - msgElement = $("##{message._id}", wrapper) - pos = wrapper.scrollTop() + msgElement.offset().top - wrapper.height()/2 - wrapper.animate({ - scrollTop: pos - }, 500) - msgElement.addClass('highlight') - - setTimeout -> - messages = wrapper[0] - instance.atBottom = messages.scrollTop >= messages.scrollHeight - messages.clientHeight; - - setTimeout -> - msgElement.removeClass('highlight') - , 500 - else - room = getRoom message.rid - room.isLoading.set true - ChatMessage.remove { rid: message.rid } - - typeName = undefined - - subscription = ChatSubscription.findOne rid: message.rid - if subscription? - ls = subscription.ls - typeName = subscription.t + subscription.name - else - curRoomDoc = ChatRoom.findOne(_id: message.rid) - typeName = curRoomDoc?.t + curRoomDoc?.name - - Meteor.call 'loadSurroundingMessages', message, limit, (err, result) -> - for item in result?.messages or [] - if item.t isnt 'command' - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - - Meteor.defer -> - readMessage.refreshUnreadMark(message.rid, true) - RoomManager.updateMentionsMarksOfRoom typeName - wrapper = $('.messages-box .wrapper') - msgElement = $("##{message._id}", wrapper) - pos = wrapper.scrollTop() + msgElement.offset().top - wrapper.height()/2 - wrapper.animate({ - scrollTop: pos - }, 500) - - msgElement.addClass('highlight') - - setTimeout -> - room.isLoading.set false - messages = wrapper[0] - instance.atBottom = !result.moreAfter && messages.scrollTop >= messages.scrollHeight - messages.clientHeight; - , 500 - - setTimeout -> - msgElement.removeClass('highlight') - , 500 - room.loaded ?= 0 - if result.messages.length? - room.loaded += result.messages.length - room.hasMore.set result.moreBefore - room.hasMoreNext.set result.moreAfter - - hasMore = (rid) -> - room = getRoom rid - - return room.hasMore.get() - - hasMoreNext = (rid) -> - room = getRoom rid - return room.hasMoreNext.get() - - - getMoreIfIsEmpty = (rid) -> - room = getRoom rid - - if room.loaded is undefined - getMore rid - - - isLoading = (rid) -> - room = getRoom rid - return room.isLoading.get() - - clear = (rid) -> - ChatMessage.remove({ rid: rid }) - if histories[rid]? - histories[rid].hasMore.set true - histories[rid].isLoading.set false - histories[rid].loaded = undefined - - getRoom: getRoom - getMore: getMore - getMoreNext: getMoreNext - getMoreIfIsEmpty: getMoreIfIsEmpty - hasMore: hasMore - hasMoreNext: hasMoreNext - isLoading: isLoading - clear: clear - getSurroundingMessages: getSurroundingMessages diff --git a/packages/rocketchat-ui/client/lib/RoomHistoryManager.js b/packages/rocketchat-ui/client/lib/RoomHistoryManager.js new file mode 100644 index 0000000000..21b9bbc830 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/RoomHistoryManager.js @@ -0,0 +1,266 @@ +/* globals readMessage UserRoles RoomRoles*/ +export const RoomHistoryManager = new class { + constructor() { + this.defaultLimit = 50; + this.histories = {}; + } + getRoom(rid) { + if ((this.histories[rid] == null)) { + this.histories[rid] = { + hasMore: new ReactiveVar(true), + hasMoreNext: new ReactiveVar(false), + isLoading: new ReactiveVar(false), + unreadNotLoaded: new ReactiveVar(0), + firstUnread: new ReactiveVar, + loaded: undefined + }; + } + + return this.histories[rid]; + } + + getMore(rid, limit) { + let ts; + if (limit == null) { limit = this.defaultLimit; } + const room = this.getRoom(rid); + if (room.hasMore.curValue !== true) { + return; + } + + room.isLoading.set(true); + + // ScrollListener.setLoader true + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: 1}}); + // lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) + + if (lastMessage != null) { + ({ ts } = lastMessage); + } else { + ts = undefined; + } + + let ls = undefined; + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid}); + if (subscription != null) { + ({ ls } = subscription); + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + return Meteor.call('loadHistory', rid, ts, limit, ls, function(err, result) { + if (err) { + return; + } + let previousHeight; + room.unreadNotLoaded.set(result.unreadNotLoaded); + room.firstUnread.set(result.firstUnread); + + const wrapper = $('.messages-box .wrapper').get(0); + if (wrapper != null) { + previousHeight = wrapper.scrollHeight; + } + + result.messages.forEach(item => { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + }); + + if (wrapper) { + const heightDiff = wrapper.scrollHeight - previousHeight; + wrapper.scrollTop += heightDiff; + } + + Meteor.defer(() => { + readMessage.refreshUnreadMark(rid, true); + return RoomManager.updateMentionsMarksOfRoom(typeName); + }); + + room.isLoading.set(false); + if (room.loaded == null) { room.loaded = 0; } + room.loaded += result.messages.length; + if (result.messages.length < limit) { + return room.hasMore.set(false); + } + }); + } + + getMoreNext(rid, limit) { + if (limit == null) { limit = this.defaultLimit; } + const room = this.getRoom(rid); + if (room.hasMoreNext.curValue !== true) { + return; + } + + const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance(); + instance.atBottom = false; + + room.isLoading.set(true); + + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: -1}}); + + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid}); + if (subscription != null) { + // const { ls } = subscription; + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + const { ts } = lastMessage; + + if (ts) { + return Meteor.call('loadNextMessages', rid, ts, limit, function(err, result) { + for (const item of Array.from((result != null ? result.messages : undefined) || [])) { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + } + + Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); + + room.isLoading.set(false); + if (room.loaded == null) { room.loaded = 0; } + + room.loaded += result.messages.length; + if (result.messages.length < limit) { + room.hasMoreNext.set(false); + } + }); + } + } + + getSurroundingMessages(message, limit) { + if (limit == null) { limit = this.defaultLimit; } + if (!(message != null ? message.rid : undefined)) { + return; + } + + const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance(); + + if (ChatMessage.findOne(message._id)) { + const wrapper = $('.messages-box .wrapper'); + const msgElement = $(`#${ message._id }`, wrapper); + const pos = (wrapper.scrollTop() + msgElement.offset().top) - (wrapper.height()/2); + wrapper.animate({ + scrollTop: pos + }, 500); + msgElement.addClass('highlight'); + + setTimeout(function() { + const messages = wrapper[0]; + return instance.atBottom = messages.scrollTop >= (messages.scrollHeight - messages.clientHeight); + }); + + return setTimeout(() => msgElement.removeClass('highlight') + , 500); + } else { + const room = this.getRoom(message.rid); + room.isLoading.set(true); + ChatMessage.remove({ rid: message.rid }); + + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid: message.rid}); + if (subscription) { + // const { ls } = subscription; + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: message.rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + return Meteor.call('loadSurroundingMessages', message, limit, function(err, result) { + for (const item of Array.from((result != null ? result.messages : undefined) || [])) { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + } + + Meteor.defer(function() { + readMessage.refreshUnreadMark(message.rid, true); + RoomManager.updateMentionsMarksOfRoom(typeName); + const wrapper = $('.messages-box .wrapper'); + const msgElement = $(`#${ message._id }`, wrapper); + const pos = (wrapper.scrollTop() + msgElement.offset().top) - (wrapper.height()/2); + wrapper.animate({ + scrollTop: pos + }, 500); + + msgElement.addClass('highlight'); + + setTimeout(function() { + room.isLoading.set(false); + const messages = wrapper[0]; + instance.atBottom = !result.moreAfter && (messages.scrollTop >= (messages.scrollHeight - messages.clientHeight)); + return 500; + }); + + return setTimeout(() => msgElement.removeClass('highlight') + , 500); + }); + if (room.loaded == null) { room.loaded = 0; } + room.loaded += result.messages.length; + room.hasMore.set(result.moreBefore); + return room.hasMoreNext.set(result.moreAfter); + }); + } + } + + hasMore(rid) { + const room = this.getRoom(rid); + return room.hasMore.get(); + } + + hasMoreNext(rid) { + const room = this.getRoom(rid); + return room.hasMoreNext.get(); + } + + + getMoreIfIsEmpty(rid) { + const room = this.getRoom(rid); + + if (room.loaded === undefined) { + return this.getMore(rid); + } + } + + + isLoading(rid) { + const room = this.getRoom(rid); + return room.isLoading.get(); + } + + clear(rid) { + ChatMessage.remove({ rid }); + if (this.histories[rid] != null) { + this.histories[rid].hasMore.set(true); + this.histories[rid].isLoading.set(false); + return this.histories[rid].loaded = undefined; + } + } +}; +this.RoomHistoryManager = RoomHistoryManager; diff --git a/packages/rocketchat-ui/client/lib/RoomManager.coffee b/packages/rocketchat-ui/client/lib/RoomManager.coffee deleted file mode 100644 index 135f512161..0000000000 --- a/packages/rocketchat-ui/client/lib/RoomManager.coffee +++ /dev/null @@ -1,253 +0,0 @@ -loadMissedMessages = (rid) -> - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: -1}, limit: 1}) - if not lastMessage? - return - - Meteor.call 'loadMissedMessages', rid, lastMessage.ts, (err, result) -> - for item in result - RocketChat.promises.run('onClientMessageReceived', item).then (item) -> - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - -connectionWasOnline = true -Tracker.autorun -> - connected = Meteor.connection.status().connected - - if connected is true and connectionWasOnline is false and RoomManager.openedRooms? - for key, value of RoomManager.openedRooms - if value.rid? - loadMissedMessages(value.rid) - - connectionWasOnline = connected - -# Reload rooms after login -currentUsername = undefined -Tracker.autorun (c) -> - user = Meteor.user() - if currentUsername is undefined and user?.username? - currentUsername = user.username - RoomManager.closeAllRooms() - FlowRouter._current.route.callAction(FlowRouter._current) - -Meteor.startup -> - ChatMessage.find().observe - removed: (record) -> - if RoomManager.getOpenedRoomByRid(record.rid)? - recordBefore = ChatMessage.findOne {ts: {$lt: record.ts}}, {sort: {ts: -1}} - if recordBefore? - ChatMessage.update {_id: recordBefore._id}, {$set: {tick: new Date}} - - recordAfter = ChatMessage.findOne {ts: {$gt: record.ts}}, {sort: {ts: 1}} - if recordAfter? - ChatMessage.update {_id: recordAfter._id}, {$set: {tick: new Date}} - - -onDeleteMessageStream = (msg) -> - ChatMessage.remove _id: msg._id - - -Tracker.autorun -> - if Meteor.userId() - RocketChat.Notifications.onUser 'message', (msg) -> - msg.u = - username: 'rocket.cat' - msg.private = true - - ChatMessage.upsert { _id: msg._id }, msg - - -@RoomManager = new class - openedRooms = {} - msgStream = new Meteor.Streamer 'room-messages' - onlineUsers = new ReactiveVar {} - - Dep = new Tracker.Dependency - - close = (typeName) -> - if openedRooms[typeName] - if openedRooms[typeName].rid? - msgStream.removeAllListeners openedRooms[typeName].rid - RocketChat.Notifications.unRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream - - openedRooms[typeName].ready = false - openedRooms[typeName].active = false - if openedRooms[typeName].template? - Blaze.remove openedRooms[typeName].template - delete openedRooms[typeName].dom - delete openedRooms[typeName].template - - rid = openedRooms[typeName].rid - delete openedRooms[typeName] - - if rid? - RoomHistoryManager.clear rid - - - computation = Tracker.autorun -> - for typeName, record of openedRooms when record.active is true - do (typeName, record) -> - - user = Meteor.user() - - if record.ready is true - return - - ready = CachedChatRoom.ready.get() and CachedChatSubscription.ready.get() is true - - if ready is true - type = typeName.substr(0, 1) - name = typeName.substr(1) - - room = Tracker.nonreactive => - return RocketChat.roomTypes.findRoom(type, name, user) - - if not room? - record.ready = true - else - openedRooms[typeName].rid = room._id - - RoomHistoryManager.getMoreIfIsEmpty room._id - record.ready = RoomHistoryManager.isLoading(room._id) is false - Dep.changed() - - if openedRooms[typeName].streamActive isnt true - openedRooms[typeName].streamActive = true - msgStream.on openedRooms[typeName].rid, (msg) -> - - RocketChat.promises.run('onClientMessageReceived', msg).then (msg) -> - - # Should not send message to room if room has not loaded all the current messages - if RoomHistoryManager.hasMoreNext(openedRooms[typeName].rid) is false - - # Do not load command messages into channel - if msg.t isnt 'command' - msg.roles = _.union(UserRoles.findOne(msg.u?._id)?.roles, RoomRoles.findOne({rid: msg.rid, 'u._id': msg.u?._id})?.roles) - ChatMessage.upsert { _id: msg._id }, msg - - Meteor.defer -> - RoomManager.updateMentionsMarksOfRoom typeName - - RocketChat.callbacks.run 'streamMessage', msg - - window.fireGlobalEvent('new-message', msg); - - RocketChat.Notifications.onRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream - - Dep.changed() - - - closeOlderRooms = -> - maxRoomsOpen = 10 - if Object.keys(openedRooms).length <= maxRoomsOpen - return - - roomsToClose = _.sortBy(_.values(openedRooms), 'lastSeen').reverse().slice(maxRoomsOpen) - for roomToClose in roomsToClose - close roomToClose.typeName - - - closeAllRooms = -> - for key, openedRoom of openedRooms - close openedRoom.typeName - - - open = (typeName) -> - if not openedRooms[typeName]? - openedRooms[typeName] = - typeName: typeName - active: false - ready: false - unreadSince: new ReactiveVar undefined - - openedRooms[typeName].lastSeen = new Date - - if openedRooms[typeName].ready - closeOlderRooms() - - if CachedChatSubscription.ready.get() is true - - if openedRooms[typeName].active isnt true - openedRooms[typeName].active = true - - computation?.invalidate() - - return { - ready: -> - Dep.depend() - return openedRooms[typeName].ready - } - - getOpenedRoomByRid = (rid) -> - for typeName, openedRoom of openedRooms - if openedRoom.rid is rid - return openedRoom - - getDomOfRoom = (typeName, rid) -> - room = openedRooms[typeName] - if not room? - return - - if not room.dom? and rid? - room.dom = document.createElement 'div' - room.dom.classList.add 'room-container' - contentAsFunc = (content) -> - return -> content - - room.template = Blaze._TemplateWith { _id: rid }, contentAsFunc(Template.room) - Blaze.render room.template, room.dom #, nextNode, parentView - - return room.dom - - existsDomOfRoom = (typeName) -> - room = openedRooms[typeName] - return room?.dom? - - updateUserStatus = (user, status, utcOffset) -> - onlineUsersValue = onlineUsers.curValue - - if status is 'offline' - delete onlineUsersValue[user.username] - else - onlineUsersValue[user.username] = - _id: user._id - status: status - utcOffset: utcOffset - - onlineUsers.set onlineUsersValue - - updateMentionsMarksOfRoom = (typeName) -> - dom = getDomOfRoom typeName - if not dom? - return - - ticksBar = $(dom).find('.ticks-bar') - $(dom).find('.ticks-bar > .tick').remove() - - scrollTop = $(dom).find('.messages-box > .wrapper').scrollTop() - 50 - totalHeight = $(dom).find('.messages-box > .wrapper > ul').height() + 40 - - $('.messages-box .mention-link-me').each (index, item) -> - topOffset = $(item).offset().top + scrollTop - percent = 100 / totalHeight * topOffset - if $(item).hasClass('mention-link-all') - ticksBar.append('
') - else - ticksBar.append('
') - - open: open - close: close - closeAllRooms: closeAllRooms - getDomOfRoom: getDomOfRoom - existsDomOfRoom: existsDomOfRoom - msgStream: msgStream - openedRooms: openedRooms - updateUserStatus: updateUserStatus - onlineUsers: onlineUsers - updateMentionsMarksOfRoom: updateMentionsMarksOfRoom - getOpenedRoomByRid: getOpenedRoomByRid - computation: computation - - -RocketChat.callbacks.add 'afterLogoutCleanUp', -> - RoomManager.closeAllRooms() -, RocketChat.callbacks.priority.MEDIUM, 'roommanager-after-logout-cleanup' diff --git a/packages/rocketchat-ui/client/lib/RoomManager.js b/packages/rocketchat-ui/client/lib/RoomManager.js new file mode 100644 index 0000000000..85ac43ee63 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/RoomManager.js @@ -0,0 +1,302 @@ +const RoomManager = new function() { + const openedRooms = {}; + const msgStream = new Meteor.Streamer('room-messages'); + const onlineUsers = new ReactiveVar({}); + const Dep = new Tracker.Dependency(); + const Cls = class { + static initClass() { + /* globals CachedChatRoom CachedChatSubscription */ + this.prototype.openedRooms = openedRooms; + this.prototype.onlineUsers = onlineUsers; + this.prototype.computation = Tracker.autorun(() => { + Object.keys(openedRooms).forEach(typeName => { + const record = openedRooms[typeName]; + if (record.active !== true || record.ready === true) { return; } + const ready = CachedChatRoom.ready.get() && CachedChatSubscription.ready.get() === true; + if (ready !== true) { return; } + const user = Meteor.user(); + + const type = typeName.substr(0, 1); + const name = typeName.substr(1); + + const room = Tracker.nonreactive(() => { + return RocketChat.roomTypes.findRoom(type, name, user); + }); + + if (room == null) { + record.ready = true; + } else { + openedRooms[typeName].rid = room._id; + + RoomHistoryManager.getMoreIfIsEmpty(room._id); + record.ready = RoomHistoryManager.isLoading(room._id) === false; + Dep.changed(); + + if (openedRooms[typeName].streamActive !== true) { + openedRooms[typeName].streamActive = true; + msgStream.on(openedRooms[typeName].rid, msg => + + RocketChat.promises.run('onClientMessageReceived', msg).then(function(msg) { + + // Should not send message to room if room has not loaded all the current messages + if (RoomHistoryManager.hasMoreNext(openedRooms[typeName].rid) === false) { + + // Do not load command messages into channel + if (msg.t !== 'command') { + const roles = [ + (msg.u && msg.u._id && UserRoles.findOne(msg.u._id, { fields: { roles: 1 }})) || {}, + (msg.u && msg.u._id && RoomRoles.findOne({rid: msg.rid, 'u._id': msg.u._id})) || {} + ].map(e => e.roles); + msg.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({ _id: msg._id }, msg); + } + + Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); + + RocketChat.callbacks.run('streamMessage', msg); + + return window.fireGlobalEvent('new-message', msg); + } + }) + ); + + RocketChat.Notifications.onRoom(openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream); // eslint-disable-line no-use-before-define + } + } + return Dep.changed(); + }); + }); + } + + getOpenedRoomByRid(rid) { + return Object.keys(openedRooms).map(typeName => openedRooms[typeName]).find(openedRoom => openedRoom.rid === rid); + } + + getDomOfRoom(typeName, rid) { + const room = openedRooms[typeName]; + if ((room == null)) { + return; + } + + if ((room.dom == null) && (rid != null)) { + room.dom = document.createElement('div'); + room.dom.classList.add('room-container'); + const contentAsFunc = content => () => content; + + room.template = Blaze._TemplateWith({ _id: rid }, contentAsFunc(Template.room)); + Blaze.render(room.template, room.dom); //, nextNode, parentView + } + + return room.dom; + } + + close(typeName) { + if (openedRooms[typeName]) { + if (openedRooms[typeName].rid != null) { + msgStream.removeAllListeners(openedRooms[typeName].rid); + RocketChat.Notifications.unRoom(openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream); // eslint-disable-line no-use-before-define + } + + openedRooms[typeName].ready = false; + openedRooms[typeName].active = false; + if (openedRooms[typeName].template != null) { + Blaze.remove(openedRooms[typeName].template); + } + delete openedRooms[typeName].dom; + delete openedRooms[typeName].template; + + const { rid } = openedRooms[typeName]; + delete openedRooms[typeName]; + + if (rid != null) { + return RoomHistoryManager.clear(rid); + } + } + } + + + closeOlderRooms() { + const maxRoomsOpen = 10; + if (Object.keys(openedRooms).length <= maxRoomsOpen) { + return; + } + + const roomsToClose = _.sortBy(_.values(openedRooms), 'lastSeen').reverse().slice(maxRoomsOpen); + return Array.from(roomsToClose).map((roomToClose) => + this.close(roomToClose.typeName)); + } + + + closeAllRooms() { + Object.keys(openedRooms).forEach(key => { + const openedRoom = openedRooms[key]; + this.close(openedRoom.typeName); + }); + } + + + open(typeName) { + if ((openedRooms[typeName] == null)) { + openedRooms[typeName] = { + typeName, + active: false, + ready: false, + unreadSince: new ReactiveVar(undefined) + }; + } + + openedRooms[typeName].lastSeen = new Date; + + if (openedRooms[typeName].ready) { + this.closeOlderRooms(); + } + + if (CachedChatSubscription.ready.get() === true) { + + if (openedRooms[typeName].active !== true) { + openedRooms[typeName].active = true; + if (this.computation) { + this.computation.invalidate(); + } + } + } + + return { + ready() { + Dep.depend(); + return openedRooms[typeName].ready; + } + }; + } + + existsDomOfRoom(typeName) { + const room = openedRooms[typeName]; + return ((room != null ? room.dom : undefined) != null); + } + + updateUserStatus(user, status, utcOffset) { + const onlineUsersValue = onlineUsers.curValue; + + if (status === 'offline') { + delete onlineUsersValue[user.username]; + } else { + onlineUsersValue[user.username] = { + _id: user._id, + status, + utcOffset + }; + } + + return onlineUsers.set(onlineUsersValue); + } + + updateMentionsMarksOfRoom(typeName) { + const dom = this.getDomOfRoom(typeName); + if ((dom == null)) { + return; + } + + const ticksBar = $(dom).find('.ticks-bar'); + $(dom).find('.ticks-bar > .tick').remove(); + + const scrollTop = $(dom).find('.messages-box > .wrapper').scrollTop() - 50; + const totalHeight = $(dom).find('.messages-box > .wrapper > ul').height() + 40; + + return $('.messages-box .mention-link-me').each(function(index, item) { + const topOffset = $(item).offset().top + scrollTop; + const percent = (100 / totalHeight) * topOffset; + if ($(item).hasClass('mention-link-all')) { + return ticksBar.append(`
`); + } else { + return ticksBar.append(`
`); + } + }); + } + }; + Cls.initClass(); + return new Cls; +}; + +const loadMissedMessages = function(rid) { + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: -1}, limit: 1}); + if (lastMessage == null) { + return; + } + + return Meteor.call('loadMissedMessages', rid, lastMessage.ts, (err, result) => + Array.from(result).map((item) => + RocketChat.promises.run('onClientMessageReceived', item).then(function(item) { + /* globals UserRoles RoomRoles*/ + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id)) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(({roles}) => roles); + item.roles = _.union.apply(_, roles); + return ChatMessage.upsert({_id: item._id}, item); + })) +); +}; + +let connectionWasOnline = true; +Tracker.autorun(function() { + const { connected } = Meteor.connection.status(); + + if (connected === true && connectionWasOnline === false && RoomManager.openedRooms != null) { + Object.keys(RoomManager.openedRooms).forEach(key => { + const value = RoomManager.openedRooms[key]; + if (value.rid != null) { + loadMissedMessages(value.rid); + } + }); + } + return connectionWasOnline = connected; +}); + +// Reload rooms after login +let currentUsername = undefined; +Tracker.autorun(() => { + const user = Meteor.user(); + if ((currentUsername === undefined) && ((user != null ? user.username : undefined) != null)) { + currentUsername = user.username; + RoomManager.closeAllRooms(); + return FlowRouter._current.route.callAction(FlowRouter._current); + } +}); + +Meteor.startup(() => + ChatMessage.find().observe({ + removed(record) { + if (RoomManager.getOpenedRoomByRid(record.rid) != null) { + const recordBefore = ChatMessage.findOne({ts: {$lt: record.ts}}, {sort: {ts: -1}}); + if (recordBefore != null) { + ChatMessage.update({_id: recordBefore._id}, {$set: {tick: new Date}}); + } + + const recordAfter = ChatMessage.findOne({ts: {$gt: record.ts}}, {sort: {ts: 1}}); + if (recordAfter != null) { + return ChatMessage.update({_id: recordAfter._id}, {$set: {tick: new Date}}); + } + } + } + }) +); + + +const onDeleteMessageStream = msg => ChatMessage.remove({_id: msg._id}); + + +Tracker.autorun(function() { + if (Meteor.userId()) { + return RocketChat.Notifications.onUser('message', function(msg) { + msg.u = + {username: 'rocket.cat'}; + msg.private = true; + + return ChatMessage.upsert({ _id: msg._id }, msg); + }); + } +}); + +export { RoomManager }; +this.RoomManager = RoomManager; +RocketChat.callbacks.add('afterLogoutCleanUp', () => RoomManager.closeAllRooms(), RocketChat.callbacks.priority.MEDIUM, 'roommanager-after-logout-cleanup'); diff --git a/packages/rocketchat-ui/client/lib/readMessages.js b/packages/rocketchat-ui/client/lib/readMessages.js index c5afd8cc08..4bd003eb65 100644 --- a/packages/rocketchat-ui/client/lib/readMessages.js +++ b/packages/rocketchat-ui/client/lib/readMessages.js @@ -11,7 +11,7 @@ // window.addEventListener 'focus', -> // readMessage.refreshUnreadMark(undefined, true) -const readMessage = new class { +const readMessage = new class { constructor() { this.debug = false; this.callbacks = []; diff --git a/packages/rocketchat-ui/client/lib/rocket.coffee b/packages/rocketchat-ui/client/lib/rocket.coffee deleted file mode 100644 index 2ff6190ef8..0000000000 --- a/packages/rocketchat-ui/client/lib/rocket.coffee +++ /dev/null @@ -1,112 +0,0 @@ -RocketChat.Login = (-> - onClick = (el) -> - $el = $(el) - if $el.length - $el.addClass "active" - $el.find("input").focus() - onBlur = (input) -> - $input = $(input) - if $input.length - if input.value == "" - $input.parents(".input-text").removeClass "active" - check = (form) -> - $form = $(form) - if $form.length - inputs = $form.find("input") - inputs.each -> - if @.value != "" - console.log @.value - $(@).parents(".input-text").addClass "active" - check: check - onClick: onClick - onBlur: onBlur - )() - -RocketChat.Button = (-> - time = undefined - loading = (el) -> - $el = $(el) - next = el.attr("data-loading-text") - html = el.find("span").html() - el.addClass("-progress").attr("data-def-text",html).find("span").html(next) - time = setTimeout -> - el.addClass("going") - , 1 - done = (el) -> - $el = $(el) - el.addClass("done") - reset = (el) -> - clearTimeout(time) if time - $el = $(el) - html= $el.attr("data-def-text") - $el.find("span").html(html) if html - $el.removeClass("-progress going done") - done: done - loading: loading - reset: reset - )() - -RocketChat.animationSupport = -> - animeEnd = - WebkitAnimation: "webkitAnimationEnd" - OAnimation: "oAnimationEnd" - msAnimation: "MSAnimationEnd" - animation: "animationend" - - transEndEventNames = - WebkitTransition: "webkitTransitionEnd" - MozTransition: "transitionend" - OTransition: "oTransitionEnd otransitionend" - msTransition: "MSTransitionEnd" - transition: "transitionend" - prefixB = transEndEventNames[Modernizr.prefixed("transition")] - prefixA = animeEnd[Modernizr.prefixed("animation")] - support = Modernizr.cssanimations - support: support - animation: prefixA - transition: prefixB - -RocketChat.animeBack = (el, callback, type) -> - el = $(el) - if not el.length > 0 - callback el if callback - return - s = animationSupport() - p = ((if type then s.animation else s.transition)) - el.one p, (e) -> - - #el.off(p); - callback e - return - - return - -RocketChat.preLoadImgs = (urls, callback) -> - L_ = (x) -> - if x.width > 0 - $(x).addClass("loaded").removeClass "loading" - loaded = $(".loaded", preLoader) - if loaded.length is urls.length and not ended - ended = 1 - imgs = preLoader.children() - callback imgs - preLoader.remove() - return - im = new Array() - preLoader = $("
").attr(id: "perverter-preloader") - loaded = undefined - ended = undefined - i = 0 - - while i < urls.length - im[i] = new Image() - im[i].onload = -> - L_ this - return - - $(im[i]).appendTo(preLoader).addClass "loading" - im[i].src = urls[i] - L_ im[i] if im[i].width > 0 - i++ - - return diff --git a/packages/rocketchat-ui/client/lib/rocket.js b/packages/rocketchat-ui/client/lib/rocket.js new file mode 100644 index 0000000000..5970e6c273 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/rocket.js @@ -0,0 +1,121 @@ +/* globals Modernizr */ +RocketChat.Login = (function() { + function onClick(el) { + const $el = $(el); + if ($el.length) { + $el.addClass('active'); + return $el.find('input').focus(); + } + } + function onBlur(input) { + const $input = $(input); + if ($input.length) { + if (input.value === '') { + return $input.parents('.input-text').removeClass('active'); + } + } + } + function check(form) { + const $form = $(form); + if ($form.length) { + const inputs = $form.find('input'); + return inputs.each(function() { + if (this.value !== '') { + console.log(this.value); + return $(this).parents('.input-text').addClass('active'); + } + }); + } + } + return { check, onClick, onBlur }; +}()); + +RocketChat.Button = (function() { + let time = undefined; + const loading = function(el) { + const next = el.attr('data-loading-text'); + const html = el.find('span').html(); + el.addClass('-progress').attr('data-def-text', html).find('span').html(next); + return time = setTimeout(() => el.addClass('going'), 1); + }; + const done = function(el) { + return el.addClass('done'); + }; + const reset = function(el) { + if (time) { clearTimeout(time); } + const $el = $(el); + const html= $el.attr('data-def-text'); + if (html) { $el.find('span').html(html); } + return $el.removeClass('-progress going done'); + }; + return { done, loading, reset }; +}()); + +RocketChat.animationSupport = function() { + const animeEnd = { + WebkitAnimation: 'webkitAnimationEnd', + OAnimation: 'oAnimationEnd', + msAnimation: 'MSAnimationEnd', + animation: 'animationend' + }; + + const transEndEventNames = { + WebkitTransition: 'webkitTransitionEnd', + MozTransition: 'transitionend', + OTransition: 'oTransitionEnd otransitionend', + msTransition: 'MSTransitionEnd', + transition: 'transitionend' + }; + const prefixB = transEndEventNames[Modernizr.prefixed('transition')]; + const prefixA = animeEnd[Modernizr.prefixed('animation')]; + const support = Modernizr.cssanimations; + return { + support, + animation: prefixA, + transition: prefixB + }; +}; + +RocketChat.animeBack = function(e, callback, type) { + const el = $(e); + if (!el.length > 0) { + if (callback) { callback(el); } + return; + } + const s = RocketChat.animationSupport(); + const p = ((type ? s.animation : s.transition)); + el.one(p, function(e) { + + //el.off(p); + callback(e); + }); + +}; + +RocketChat.preLoadImgs = function(urls, callback) { + const preLoader = $('
').attr({id: 'perverter-preloader'}); + let ended = undefined; + const l_ = function(x) { + if (x.width > 0) { + $(x).addClass('loaded').removeClass('loading'); + const loaded = $('.loaded', preLoader); + if ((loaded.length === urls.length) && !ended) { + ended = 1; + const imgs = preLoader.children(); + callback(imgs); + preLoader.remove(); + } + } + }; + return urls.map(url => { + const im = new Image(); + im.onload = function() { + l_(this); + }; + $(im).appendTo(preLoader).addClass('loading'); + im.src = url; + if (im.width > 0) { l_(im); } + return im; + }); + +}; diff --git a/packages/rocketchat-ui/client/views/app/room.js b/packages/rocketchat-ui/client/views/app/room.js index f6e21d9015..875b28bbfb 100644 --- a/packages/rocketchat-ui/client/views/app/room.js +++ b/packages/rocketchat-ui/client/views/app/room.js @@ -220,7 +220,7 @@ Template.room.helpers({ canPreview() { const room = Session.get(`roomData${ this._id }`); - if (room.t !== 'c') { + if (room && room.t !== 'c') { return true; } diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index f9ad4248d5..44831edb83 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -42,15 +42,15 @@ Package.onUse(function(api) { api.addFiles('client/lib/fireEvent.js', 'client'); api.addFiles('client/lib/iframeCommands.js', 'client'); api.addFiles('client/lib/menu.js', 'client'); - api.addFiles('client/lib/modal.coffee', 'client'); + api.addFiles('client/lib/modal.js', 'client'); api.addFiles('client/lib/Modernizr.js', 'client'); api.addFiles('client/lib/msgTyping.js', 'client'); api.addFiles('client/lib/notification.js', 'client'); api.addFiles('client/lib/parentTemplate.js', 'client'); api.addFiles('client/lib/readMessages.js', 'client'); - api.addFiles('client/lib/rocket.coffee', 'client'); - api.addFiles('client/lib/RoomHistoryManager.coffee', 'client'); - api.addFiles('client/lib/RoomManager.coffee', 'client'); + api.addFiles('client/lib/rocket.js', 'client'); + api.addFiles('client/lib/RoomHistoryManager.js', 'client'); + api.addFiles('client/lib/RoomManager.js', 'client'); api.addFiles('client/lib/sideNav.js', 'client'); api.addFiles('client/lib/tapi18n.js', 'client'); api.addFiles('client/lib/textarea-autogrow.js', 'client'); -- GitLab From ebc381aa3bca05857f826a38c9884c7992c8889f Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Mon, 8 May 2017 14:18:00 -0300 Subject: [PATCH 046/280] Fixes removing rooms from roomPick type of settings Close #6646 --- packages/rocketchat-ui-admin/client/admin.coffee | 5 ++++- packages/rocketchat-ui-admin/client/admin.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/rocketchat-ui-admin/client/admin.coffee b/packages/rocketchat-ui-admin/client/admin.coffee index 3abde676b3..5f86c8d750 100644 --- a/packages/rocketchat-ui-admin/client/admin.coffee +++ b/packages/rocketchat-ui-admin/client/admin.coffee @@ -19,7 +19,10 @@ setFieldValue = (settingId, value, type, editor) -> if editor is 'color' new jscolor(input) - + when 'roomPick' + selectedRooms = Template.instance().selectedRooms.get() + selectedRooms[settingId] = value + Template.instance().selectedRooms.set(selectedRooms) else input.val(value).change() diff --git a/packages/rocketchat-ui-admin/client/admin.html b/packages/rocketchat-ui-admin/client/admin.html index 38a6a2b1e0..e7f3cbd8bd 100644 --- a/packages/rocketchat-ui-admin/client/admin.html +++ b/packages/rocketchat-ui-admin/client/admin.html @@ -179,7 +179,7 @@ {{> inputAutocomplete settings=autocompleteRoom id=_id name=_id class="search autocomplete" autocomplete="off" disabled=isDisabled.disabled}}
    {{#each selectedRooms}} -
  • {{name}}
  • +
  • {{name}}
  • {{/each}}
-- GitLab From c40e03de99ea3913f7f1fa685c9e06c57888dd74 Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Mon, 8 May 2017 15:50:24 -0300 Subject: [PATCH 047/280] Fix slackbridge text replacements --- packages/rocketchat-slackbridge/slackbridge.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index 2dd320a735..42a3ea70f1 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -80,8 +80,9 @@ class SlackBridge { if (!_.isEmpty(slackMsgTxt)) { slackMsgTxt = slackMsgTxt.replace(//g, '@all'); slackMsgTxt = slackMsgTxt.replace(//g, '@all'); - slackMsgTxt = slackMsgTxt.replace(/>/g, '<'); - slackMsgTxt = slackMsgTxt.replace(/</g, '>'); + slackMsgTxt = slackMsgTxt.replace(//g, '@here'); + slackMsgTxt = slackMsgTxt.replace(/>/g, '>'); + slackMsgTxt = slackMsgTxt.replace(/</g, '<'); slackMsgTxt = slackMsgTxt.replace(/&/g, '&'); slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:'); slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:'); -- GitLab From b3f07535b9c42b35ae7b8538b22b4112a91211fd Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 8 May 2017 16:15:57 -0300 Subject: [PATCH 048/280] conversion --- .../client/message.coffee | 254 ------------ .../rocketchat-ui-message/client/message.js | 142 +++---- .../client/messageBox.coffee | 391 ------------------ .../client/messageBox.js | 6 +- .../client/popup/messagePopup.coffee | 282 ------------- .../client/popup/messagePopupConfig.coffee | 235 ----------- .../client/popup/messagePopupEmoji.coffee | 4 - packages/rocketchat-ui-message/package.js | 3 +- 8 files changed, 72 insertions(+), 1245 deletions(-) delete mode 100644 packages/rocketchat-ui-message/client/message.coffee delete mode 100644 packages/rocketchat-ui-message/client/messageBox.coffee delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopup.coffee delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee diff --git a/packages/rocketchat-ui-message/client/message.coffee b/packages/rocketchat-ui-message/client/message.coffee deleted file mode 100644 index 2684dfd9c0..0000000000 --- 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 index 9129677d94..ae7997d583 100644 --- a/packages/rocketchat-ui-message/client/message.js +++ b/packages/rocketchat-ui-message/client/message.js @@ -1,6 +1,10 @@ +/* globals renderEmoji renderMessageBody*/ import moment from 'moment'; Template.message.helpers({ + encodeURI(text) { + return encodeURI(text); + }, isBot() { if (this.bot != null) { return 'bot'; @@ -103,8 +107,8 @@ Template.message.helpers({ } }, showTranslated() { - if (RocketChat.settings.get('AutoTranslate_Enabled') && ((ref = this.u) != null ? ref._id : void 0) !== Meteor.userId() && !RocketChat.MessageTypes.isSystemMessage(this)) { - subscription = RocketChat.models.Subscriptions.findOne({ + 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() }, { @@ -114,7 +118,7 @@ Template.message.helpers({ } }); const language = RocketChat.AutoTranslate.getLanguage(this.rid); - return this.autoTranslateFetching || ((subscription != null ? subscription.autoTranslate : void 0) !== this.autoTranslateShowInverse && this.translations && this.translations[language]); + return this.autoTranslateFetching || subscription && subscription.autoTranslate !== this.autoTranslateShowInverse && this.translations && this.translations[language]; } }, edited() { @@ -137,15 +141,17 @@ Template.message.helpers({ canEdit() { const hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', this.rid); const isEditAllowed = RocketChat.settings.get('Message_AllowEditing'); - const editOwn = ((ref = this.u) != null ? ref._id : void 0) === Meteor.userId(); + const editOwn = this.u && this.u._id === Meteor.userId(); if (!(hasPermission || (isEditAllowed && editOwn))) { return; } const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); - if ((blockEditInMinutes != null) && blockEditInMinutes !== 0) { + if (blockEditInMinutes) { + let msgTs; if (this.ts != null) { msgTs = moment(this.ts); } + let currentTsDiff; if (msgTs != null) { currentTsDiff = moment().diff(msgTs, 'minutes'); } @@ -155,18 +161,19 @@ Template.message.helpers({ } }, canDelete() { - let blockDeleteInMinutes, currentTsDiff, deleteOwn, hasPermission, isDeleteAllowed, msgTs, ref; - hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid); - isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); - deleteOwn = ((ref = this.u) != null ? ref._id : void 0) === Meteor.userId(); + 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; } - blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0) { + 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'); } @@ -186,27 +193,20 @@ Template.message.helpers({ } }, hasOembed() { - let ref, ref1, ref2, ref3; - if (!(((ref = this.urls) != null ? ref.length : void 0) > 0 && (Template.oembedBaseWidget != null) && RocketChat.settings.get('API_Embed'))) { + if (!(this.urls && this.urls.length > 0 && Template.oembedBaseWidget != null && RocketChat.settings.get('API_Embed'))) { return false; } - if (ref1 = (ref2 = this.u) != null ? ref2.username : void 0, indexOf.call((ref3 = RocketChat.settings.get('API_EmbedDisabledFor')) != null ? ref3.split(',').map(function(username) { - return username.trim(); - }) : void 0, ref1) >= 0) { + if (!(RocketChat.settings.get('API_EmbedDisabledFor')||'').split(',').map(username => username.trim()).includes(this.u && this.u.username)) { return false; } return true; }, reactions() { - let emoji, msgReactions, reaction, ref, total, userUsername, usernames; - msgReactions = []; - userUsername = Meteor.user().username; - ref = this.reactions; - for (emoji in ref) { - reaction = ref[emoji]; - total = reaction.usernames.length; - usernames = `@${ reaction.usernames.slice(0, 15).join(', @') }`; - usernames = usernames.replace(`@${ userUsername }`, t('You').toLowerCase()); + 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 @@ -217,15 +217,14 @@ Template.message.helpers({ if (usernames[0] !== '@') { usernames = usernames[0].toUpperCase() + usernames.substr(1); } - msgReactions.push({ + return { emoji, count: reaction.usernames.length, usernames, reaction: ` ${ t('Reacted_with').toLowerCase() } ${ emoji }`, userReacted: reaction.usernames.indexOf(userUsername) > -1 - }); - } - return msgReactions; + }; + }); }, markUserReaction(reaction) { if (reaction.userReacted) { @@ -256,73 +255,63 @@ Template.message.helpers({ data.index = index; }, hideCog() { - let subscription; - subscription = RocketChat.models.Subscriptions.findOne({ + const subscription = RocketChat.models.Subscriptions.findOne({ rid: this.rid }); if (subscription == null) { return 'hidden'; } - }, - hideUsernames() { - let prefs, ref, ref1; - prefs = (ref = Meteor.user()) != null ? (ref1 = ref.settings) != null ? ref1.preferences : void 0 : void 0; - if (prefs != null ? prefs.hideUsernames : void 0) { - - } } }); Template.message.onCreated(function() { - let msg; - msg = Template.currentData(); + let msg = Template.currentData(); + this.wasEdited = (msg.editedAt != null) && !RocketChat.MessageTypes.isSystemMessage(msg); - return this.body = (function() { - let isSystemMessage, messageType, ref; - isSystemMessage = RocketChat.MessageTypes.isSystemMessage(msg); - messageType = RocketChat.MessageTypes.getType(msg); - if ((messageType != null ? messageType.render : void 0) != null) { - msg = messageType.render(msg); - } else if ((messageType != null ? messageType.template : void 0) != null) { - } else if ((messageType != null ? messageType.message : void 0) != null) { - if ((typeof messageType.data === 'function' ? messageType.data(msg) : void 0) != null) { + 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 (((ref = msg.u) != null ? ref.username : void 0) === RocketChat.settings.get('Chatops_Username')) { + } 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) { - return RocketChat.Markdown(msg); - } else { - return msg; + msg.html = RocketChat.Markdown.parse(msg.html); } - }()); + return msg; + })(); }); Template.message.onViewRendered = function(context) { - let view; - view = this; return this._domrange.onAttached(function(domRange) { - let $currentNode, $nextNode, currentDataset, currentMessageDate, currentNode, newMessage, nextDataset, nextNode, previousDataset, previousMessageDate, previousNode, ref, templateInstance; - currentNode = domRange.lastNode(); - currentDataset = currentNode.dataset; - previousNode = currentNode.previousElementSibling; - nextNode = currentNode.nextElementSibling; - $currentNode = $(currentNode); - $nextNode = $(nextNode); + 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 != null ? previousNode.dataset : void 0) != null) { - previousDataset = previousNode.dataset; - previousMessageDate = new Date(parseInt(previousDataset.timestamp)); - currentMessageDate = new Date(parseInt(currentDataset.timestamp)); + } 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 { @@ -336,8 +325,8 @@ Template.message.onViewRendered = function(context) { $currentNode.addClass('sequential'); } } - if ((nextNode != null ? nextNode.dataset : void 0) != null) { - nextDataset = nextNode.dataset; + if (nextNode && nextNode.dataset) { + const nextDataset = nextNode.dataset; if (nextDataset.date !== currentDataset.date) { $nextNode.addClass('new-day').removeClass('sequential'); } else { @@ -352,12 +341,17 @@ Template.message.onViewRendered = function(context) { } } if (nextNode == null) { - templateInstance = $(`#chat-window-${ context.rid }`)[0] ? (ref = Blaze.getView($(`#chat-window-${ context.rid }`)[0])) != null ? ref.templateInstance() : void 0 : 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 != null ? templateInstance.atBottom = true : void 0; - } else if ((templateInstance != null ? templateInstance.firstNode : void 0) && (templateInstance != null ? templateInstance.atBottom : void 0) === false) { - newMessage = templateInstance != null ? templateInstance.find('.new-message') : void 0; - return newMessage != null ? newMessage.className = 'new-message background-primary-action-color color-content-background-color ' : void 0; + 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 2587fda0c3..0000000000 --- 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 = """ -
- -
- """ - - 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 index f882b3bc0f..add30a1221 100644 --- a/packages/rocketchat-ui-message/client/messageBox.js +++ b/packages/rocketchat-ui-message/client/messageBox.js @@ -274,7 +274,7 @@ Template.messageBox.events({ if (e.originalEvent.clipboardData == null) { return; } - const items = e.originalEvent.clipboardData.items; + const items = [...e.originalEvent.clipboardData.items]; const files = items.map(item => { if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { e.preventDefault(); @@ -283,7 +283,7 @@ Template.messageBox.events({ name: `Clipboard - ${ moment().format(RocketChat.settings.get('Message_TimeAndDateFormat')) }` }; } - }).filter(); + }).filter(e => e); if (files.length) { return fileUpload(files); } else { @@ -313,7 +313,7 @@ Template.messageBox.events({ if (!files || files.length === 0) { files = (e.dataTransfer && e.dataTransfer.files) || []; } - const filesToUpload = files.map(file => { + const filesToUpload = [...files].map(file => { // `file.type = mime.lookup(file.name)` does not work. Object.defineProperty(file, 'type', { value: mime.lookup(file.name) 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 c76fcf1b17..0000000000 --- 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/messagePopupConfig.coffee b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee deleted file mode 100644 index a5bc17cc33..0000000000 --- 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/messagePopupEmoji.coffee b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee deleted file mode 100644 index d50a31f2c2..0000000000 --- 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/package.js b/packages/rocketchat-ui-message/package.js index 881c8687a1..bcf2b9e3dd 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,7 +32,7 @@ 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/message.js', 'client'); api.addFiles('client/messageBox.js', 'client'); api.addFiles('client/popup/messagePopup.js', 'client'); api.addFiles('client/popup/messagePopupChannel.js', 'client'); -- GitLab From 3f1855edbb3cd1c08c2ba43ec7a866cdc369a566 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 8 May 2017 16:29:01 -0300 Subject: [PATCH 049/280] start the conversion on meteor-autocomplete and move the files to a client folder --- .../client/autocomplete-client.coffee | 369 ++++++++++++++++ .../client/autocomplete-client.js | 416 ++++++++++++++++++ .../client/autocomplete.css | 27 ++ .../meteor-autocomplete/client/inputs.html | 39 ++ .../client/templates.coffee | 50 +++ packages/meteor-autocomplete/package.js | 10 +- .../server/autocomplete-server.coffee | 27 ++ 7 files changed, 933 insertions(+), 5 deletions(-) create mode 100755 packages/meteor-autocomplete/client/autocomplete-client.coffee create mode 100755 packages/meteor-autocomplete/client/autocomplete-client.js create mode 100755 packages/meteor-autocomplete/client/autocomplete.css create mode 100755 packages/meteor-autocomplete/client/inputs.html create mode 100755 packages/meteor-autocomplete/client/templates.coffee create mode 100755 packages/meteor-autocomplete/server/autocomplete-server.coffee diff --git a/packages/meteor-autocomplete/client/autocomplete-client.coffee b/packages/meteor-autocomplete/client/autocomplete-client.coffee new file mode 100755 index 0000000000..1996c1df53 --- /dev/null +++ b/packages/meteor-autocomplete/client/autocomplete-client.coffee @@ -0,0 +1,369 @@ +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(".") + console.log(obj) + 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 + console.log(@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/client/autocomplete-client.js b/packages/meteor-autocomplete/client/autocomplete-client.js new file mode 100755 index 0000000000..6c2f9d902b --- /dev/null +++ b/packages/meteor-autocomplete/client/autocomplete-client.js @@ -0,0 +1,416 @@ +/* globals Deps */ +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'); + } + if (rule.callback != null) { + return console.warn('autocomplete no longer supports callbacks; use event listeners instead.'); + } +}; + +const isWholeField = function(rule) { + return !rule.token; +}; + +const getRegExp = function(rule) { + if (!isWholeField(rule)) { + return new RegExp(`(^|\\b|\\s)${ rule.token }([\\w.]*)$`); + } else { + return new RegExp('(^)(.*)$'); + } +}; + +const getFindParams = function(rule, filter, limit) { + const selector = _.extend({}, rule.filter || {}); + const options = { + limit + }; + if (!filter) { + return [selector, options]; + } + if (rule.sort && rule.field) { + const sortspec = {}; + sortspec[rule.field] = 1; + options.sort = sortspec; + } + if (_.isFunction(rule.selector)) { + _.extend(selector, rule.selector(filter)); + } else { + selector[rule.field] = { + $regex: rule.matchAll ? filter : `^${ filter }`, + $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 = (function() { + const results = []; + Object.keys(rules).forEach((key) => { + const rule = rules[key]; + results.push(getRegExp(rule)); + }); + return results; + }); + this.matched = -1; + this.loaded = true; + this.ruleDep = new Deps.Dependency; + this.filterDep = new Deps.Dependency; + this.loadingDep = new Deps.Dependency; + this.sub = null; + this.comp = Deps.autorun((function(_this) { + return function() { + let filter, options, ref1, ref2, selector, subName; + if ((ref1 = _this.sub) != null) { + ref1.stop(); + } + if (!((rule = _this.matchedRule()) && (filter = _this.getFilter()) !== null)) { + return; + } + if (!isServerSearch(rule)) { + _this.setLoaded(true); + return; + } + ref2 = getFindParams(rule, filter, _this.limit), selector = ref2[0], options = ref2[1]; + _this.setLoaded(false); + subName = rule.subscription || 'autocomplete-recordset'; + return _this.sub = Meteor.subscribe(subName, selector, options, rule.collection, function() { + return _this.setLoaded(true); + }); + }; + })(this)); + } + + teardown() { + return this.comp.stop(); + } + + matchedRule() { + this.ruleDep.depend(); + if (this.matched >= 0) { + return this.rules[this.matched]; + } else { + return null; + } + } + + setMatchedRule(i) { + this.matched = i; + return 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; + } + this.loaded = val; + return this.loadingDep.changed(); + } + + onKeyUp() { + let breakLoop, i, matches, results, startpos, val; + if (!this.$element) { + return; + } + startpos = this.element.selectionStart; + 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. + */ + i = 0; + breakLoop = false; + results = []; + while (i < this.expressions.length) { + matches = val.match(this.expressions[i]); + if (!matches && this.matched === i) { + this.setMatchedRule(-1); + breakLoop = true; + } + if (matches && this.matched === -1) { + this.setMatchedRule(i); + breakLoop = true; + } + if (matches && this.filter !== matches[2]) { + this.setFilter(matches[2]); + breakLoop = true; + } + if (breakLoop) { + break; + } + results.push(i++); + } + return results; + } + + onKeyDown(e) { + if (this.matched === -1 || (this.constructor.KEYS.indexOf(e.keyCode) < 0)) { + return; + } + switch (e.keyCode) { + case 9: + case 13: + if (this.select()) { + e.preventDefault(); + e.stopPropagation(); + } + break; + case 40: + e.preventDefault(); + this.next(); + break; + case 38: + e.preventDefault(); + this.prev(); + break; + case 27: + this.$element.blur(); + this.hideList(); + } + } + + onFocus() { + return Meteor.defer((function(_this) { + return function() { + return _this.onKeyUp(); + }; + })(this)); + } + + onBlur() { + return Meteor.setTimeout((function(_this) { + return function() { + return _this.hideList(); + }; + }(this)), 500); + } + + onItemClick(doc, e) { + return this.processSelection(doc, this.rules[this.matched]); + } + + onItemHover(doc, e) { + this.tmplInst.$('.-autocomplete-item').removeClass('selected'); + return $(e.target).closest('.-autocomplete-item').addClass('selected'); + } + + filteredList() { + let filter, options, ref, rule, selector; + filter = this.getFilter(); + if (this.matched === -1) { + return null; + } + rule = this.rules[this.matched]; + if (!(rule.token || filter)) { + return null; + } + ref = getFindParams(rule, filter, this.limit), selector = ref[0], options = ref[1]; + Meteor.defer((function(_this) { + return function() { + return _this.ensureSelection(); + }; + })(this)); + if (isServerSearch(rule)) { + return AutoCompleteRecords.find({}, options); + } + return rule.collection.find(selector, options); + } + + isShowing() { + let rule, showing; + rule = this.matchedRule(); + showing = (rule != null) && (rule.token || this.getFilter()); + if (showing) { + Meteor.defer((function(_this) { + return function() { + _this.positionContainer(); + return _this.ensureSelection(); + }; + })(this)); + } + return showing; + } + + select() { + let doc, node; + node = this.tmplInst.find('.-autocomplete-item.selected'); + if (node == null) { + return false; + } + doc = Blaze.getData(node); + if (!doc) { + return false; + } + this.processSelection(doc, this.rules[this.matched]); + return true; + } + + processSelection(doc, rule) { + let replacement; + replacement = getField(doc, rule.field); + if (!isWholeField(rule)) { + this.replace(replacement, rule); + this.hideList(); + } else { + this.setText(replacement); + this.onBlur(); + } + this.$element.trigger('autocompleteselect', doc); + } + + replace(replacement) { + let finalFight, fullStuff, newPosition, posfix, separator, startpos, val; + startpos = this.element.selectionStart; + fullStuff = this.getText(); + val = fullStuff.substring(0, startpos); + val = val.replace(this.expressions[this.matched], `$1${ this.rules[this.matched].token }${ replacement }`); + posfix = fullStuff.substring(startpos, fullStuff.length); + separator = (posfix.match(/^\s/) ? '' : ' '); + finalFight = val + separator + posfix; + this.setText(finalFight); + newPosition = val.length + 1; + this.element.setSelectionRange(newPosition, newPosition); + } + + hideList() { + this.setMatchedRule(-1); + return this.setFilter(null); + } + + getText() { + return this.$element.val() || this.$element.text(); + } + + setText(text) { + if (this.$element.is('input,textarea')) { + return this.$element.val(text); + } else { + return this.$element.html(text); + } + } + + + /* + Rendering functions + */ + + positionContainer() { + let offset, pos, position, rule; + position = this.$element.position(); + rule = this.matchedRule(); + offset = getCaretCoordinates(this.element, this.element.selectionStart); + if ((rule != null) && isWholeField(rule)) { + pos = { + left: position.left, + width: this.$element.outerWidth() + }; + } else { + pos = { + left: position.left + offset.left + }; + } + 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')); + } + return this.tmplInst.$('.-autocomplete-container').css(pos); + } + + ensureSelection() { + let selectedItem; + selectedItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!selectedItem.length) { + return this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + next() { + let currentItem, next; + currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; + } + currentItem.removeClass('selected'); + next = currentItem.next(); + if (next.length) { + return next.addClass('selected'); + } else { + return this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + prev() { + let currentItem, prev; + currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; + } + currentItem.removeClass('selected'); + prev = currentItem.prev(); + if (prev.length) { + return prev.addClass('selected'); + } else { + return this.tmplInst.$('.-autocomplete-item:last-child').addClass('selected'); + } + } + + currentTemplate() { + return this.rules[this.matched].template; + } + + + +}; + +const AutocompleteTest = { + records: AutoCompleteRecords, + getRegExp, + getFindParams +}; diff --git a/packages/meteor-autocomplete/client/autocomplete.css b/packages/meteor-autocomplete/client/autocomplete.css new file mode 100755 index 0000000000..c4af66c2ff --- /dev/null +++ b/packages/meteor-autocomplete/client/autocomplete.css @@ -0,0 +1,27 @@ +.-autocomplete-container { + position: absolute; + background: white; + border: 1px solid #DDD; + border-radius: 3px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + min-width: 180px; + z-index: 1000; +} + +.-autocomplete-list { + list-style: none; + margin: 0; + padding: 0; +} + +.-autocomplete-item { + display: block; + padding: 5px 10px; + border-bottom: 1px solid #DDD; +} + +.-autocomplete-item.selected { + color: white; + background: #4183C4; + text-decoration: none; +} diff --git a/packages/meteor-autocomplete/client/inputs.html b/packages/meteor-autocomplete/client/inputs.html new file mode 100755 index 0000000000..289caa58ff --- /dev/null +++ b/packages/meteor-autocomplete/client/inputs.html @@ -0,0 +1,39 @@ + + + + + + + diff --git a/packages/meteor-autocomplete/client/templates.coffee b/packages/meteor-autocomplete/client/templates.coffee new file mode 100755 index 0000000000..cd56d8ba73 --- /dev/null +++ b/packages/meteor-autocomplete/client/templates.coffee @@ -0,0 +1,50 @@ +# 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/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js index 5a6912af07..8694c20acc 100755 --- a/packages/meteor-autocomplete/package.js +++ b/packages/meteor-autocomplete/package.js @@ -14,14 +14,14 @@ Package.onUse(function(api) { // Our files api.addFiles([ - 'autocomplete.css', - 'inputs.html', - 'autocomplete-client.coffee', - 'templates.coffee' + 'client/autocomplete.css', + 'client/inputs.html', + 'client/autocomplete-client.coffee', + 'client/templates.coffee' ], 'client'); api.addFiles([ - 'autocomplete-server.coffee' + 'server/autocomplete-server.coffee' ], 'server'); api.export('Autocomplete', 'server'); diff --git a/packages/meteor-autocomplete/server/autocomplete-server.coffee b/packages/meteor-autocomplete/server/autocomplete-server.coffee new file mode 100755 index 0000000000..192d5479bb --- /dev/null +++ b/packages/meteor-autocomplete/server/autocomplete-server.coffee @@ -0,0 +1,27 @@ +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() + -- GitLab From 896f27d2397ad1f326e58cd70bfc821e06b62c6a Mon Sep 17 00:00:00 2001 From: Gabriel Engel Date: Mon, 8 May 2017 16:45:51 -0300 Subject: [PATCH 050/280] dependencies upgrade --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5238246908..dc56f1e493 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "babel-runtime": "^6.23.0", "bcrypt": "^1.0.2", "codemirror": "^5.25.2", - "file-type": "^4.2.0", + "file-type": "^4.3.0", "highlight.js": "^9.11.0", "jquery": "^3.2.1", "mime-db": "^1.27.0", @@ -74,7 +74,7 @@ "moment": "^2.18.1", "moment-timezone": "^0.5.13", "photoswipe": "^4.1.2", - "prom-client": "^8.1.1", + "prom-client": "^9.0.0", "semver": "^5.3.0", "toastr": "^2.1.2" } -- GitLab From 996f46ec5f03105ce8d2ea1d536a94a290a78319 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 07:34:04 -0400 Subject: [PATCH 051/280] Adding delete-any-message permission and updates to ui --- client/methods/deleteMessage.js | 10 +- .../server/startup.js | 1 + .../.npm/package/npm-shrinkwrap.json | 192 +++++++++++++++--- .../rocketchat-lib/client/MessageAction.js | 5 +- .../server/methods/deleteMessage.js | 6 +- .../client/lib/chatMessages.coffee | 3 +- 6 files changed, 177 insertions(+), 40 deletions(-) diff --git a/client/methods/deleteMessage.js b/client/methods/deleteMessage.js index e7acb1c30a..deb5789f16 100644 --- a/client/methods/deleteMessage.js +++ b/client/methods/deleteMessage.js @@ -10,19 +10,21 @@ Meteor.methods({ //We're now only passed in the `_id` property to lower the amount of data sent to the server message = ChatMessage.findOne({ _id: message._id }); + // should use only hasPermission and change delete-message to array of the + // two desired permissions? const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); + const deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); let deleteOwn = false; + if (message && message.u && message.u._id) { deleteOwn = message.u._id === Meteor.userId(); } - - if (!(hasPermission || (deleteAllowed && deleteOwn))) { + if (!(deleteAny || hasPermission || (deleteAllowed && deleteOwn))) { return false; } - const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if (_.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0) { + if (!(deleteAny) || (_.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0)) { if (message.ts) { const msgTs = moment(message.ts); if (msgTs) { diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 6aeb68290d..a1eae521a1 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -24,6 +24,7 @@ Meteor.startup(function() { { _id: 'delete-c', roles : ['admin'] }, { _id: 'delete-d', roles : ['admin'] }, { _id: 'delete-message', roles : ['admin', 'owner', 'moderator'] }, + { _id: 'delete-any-message', roles : ['admin', 'owner'] }, { _id: 'delete-p', roles : ['admin'] }, { _id: 'delete-user', roles : ['admin'] }, { _id: 'edit-message', roles : ['admin', 'owner', 'moderator'] }, 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 c931564c62..0b44b80161 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -30,6 +30,11 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "from": "arrify@>=1.0.1 <2.0.0" }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "from": "ascli@>=1.0.0 <2.0.0" + }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -60,6 +65,11 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "from": "aws4@>=1.2.1 <2.0.0" }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "from": "balanced-match@>=0.4.1 <0.5.0" + }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", @@ -75,6 +85,11 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "from": "boom@>=2.0.0 <3.0.0" }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "from": "brace-expansion@>=1.1.7 <2.0.0" + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -85,6 +100,16 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "from": "buffer-shims@>=1.0.0 <1.1.0" }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "from": "bytebuffer@>=5.0.0 <6.0.0" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "from": "camelcase@>=2.0.1 <3.0.0" + }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -100,11 +125,26 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "from": "chalk@>=1.1.1 <2.0.0" }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "from": "cliui@>=3.0.3 <4.0.0" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "from": "co@>=4.6.0 <5.0.0" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "from": "code-point-at@>=1.0.0 <2.0.0" + }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "from": "colour@>=0.7.1 <0.8.0" + }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -115,6 +155,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "from": "commander@>=2.9.0 <3.0.0" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "from": "concat-map@0.0.1" + }, "concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", @@ -147,6 +192,11 @@ } } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "from": "decamelize@>=1.1.1 <2.0.0" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -202,6 +252,11 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", "from": "form-data@>=2.1.1 <2.2.0" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "from": "fs.realpath@>=1.0.0 <2.0.0" + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -224,6 +279,11 @@ } } }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "from": "glob@>=7.0.5 <8.0.0" + }, "google-auth-library": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz", @@ -262,8 +322,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.0.tgz", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.1.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -511,8 +571,8 @@ } }, "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "from": "extend@>=3.0.0 <3.1.0" }, "forever-agent": { @@ -538,8 +598,8 @@ "from": "har-validator@>=4.2.1 <4.3.0", "dependencies": { "ajv": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "from": "ajv@>=4.9.1 <5.0.0", "dependencies": { "co": { @@ -879,8 +939,8 @@ "from": "tar-pack@>=3.4.0 <4.0.0", "dependencies": { "debug": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.5.tgz", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz", "from": "debug@>=2.2.0 <3.0.0", "dependencies": { "ms": { @@ -903,7 +963,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" } } }, @@ -946,7 +1006,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.3.3 <2.0.0", + "from": "once@>=1.0.0 <2.0.0", "dependencies": { "wrappy": { "version": "1.0.2", @@ -973,7 +1033,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" }, "isarray": { "version": "1.0.0", @@ -1043,16 +1103,31 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "from": "http-signature@>=1.1.0 <1.2.0" }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "from": "inflight@>=1.0.4 <2.0.0" + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "from": "inherits@>=2.0.3 <3.0.0" }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "from": "invert-kv@>=1.0.0 <2.0.0" + }, "is": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", "from": "is@>=3.0.1 <4.0.0" }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0" + }, "is-my-json-valid": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", @@ -1063,11 +1138,6 @@ "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.0", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.0.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", @@ -1140,6 +1210,11 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", "from": "jws@>=3.1.4 <4.0.0" }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "from": "lcid@>=1.0.0 <2.0.0" + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -1158,7 +1233,7 @@ "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "from": "long@>=3.2.0 <4.0.0" + "from": "long@>=3.0.0 <4.0.0" }, "methmeth": { "version": "1.1.0", @@ -1180,6 +1255,11 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "from": "mime-types@>=2.1.7 <2.2.0" }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "from": "minimatch@>=3.0.2 <4.0.0" + }, "modelo": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz", @@ -1195,6 +1275,11 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "from": "node-forge@>=0.7.1 <0.8.0" }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "from": "number-is-nan@>=1.0.0 <2.0.0" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -1210,6 +1295,21 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "from": "once@>=1.3.0 <1.4.0" }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "from": "optjs@>=3.2.2 <3.3.0" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "from": "os-locale@>=1.4.0 <2.0.0" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "from": "path-is-absolute@>=1.0.0 <2.0.0" + }, "performance-now": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", @@ -1236,9 +1336,9 @@ "from": "propprop@>=0.3.1 <0.4.0" }, "protobufjs": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.7.3.tgz", - "from": "protobufjs@>=6.7.0 <7.0.0" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", + "from": "protobufjs@>=5.0.0 <6.0.0" }, "punycode": { "version": "1.4.1", @@ -1308,14 +1408,19 @@ "from": "sntp@>=1.0.0 <2.0.0" }, "split-array-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.0.tgz", + "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": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "from": "async@>=1.4.0 <2.0.0" + "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" } } }, @@ -1332,8 +1437,8 @@ } }, "stream-events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.1.tgz", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", "from": "stream-events@>=1.0.1 <2.0.0" }, "stream-shift": { @@ -1346,6 +1451,11 @@ "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz", "from": "string-format-obj@>=1.1.0 <2.0.0" }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "from": "string-width@>=1.0.1 <2.0.0" + }, "string_decoder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz", @@ -1362,9 +1472,9 @@ "from": "strip-ansi@>=3.0.0 <4.0.0" }, "stubs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-1.1.2.tgz", - "from": "stubs@>=1.1.0 <2.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "from": "stubs@>=3.0.0 <4.0.0" }, "supports-color": { "version": "2.0.0", @@ -1411,6 +1521,16 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "from": "verror@1.3.6" }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "from": "window-size@>=0.1.4 <0.2.0" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "from": "wrap-ansi@>=2.0.0 <3.0.0" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1420,6 +1540,16 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "from": "xtend@>=4.0.0 <5.0.0" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "from": "y18n@>=3.2.0 <4.0.0" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "from": "yargs@>=3.10.0 <4.0.0" } } } diff --git a/packages/rocketchat-lib/client/MessageAction.js b/packages/rocketchat-lib/client/MessageAction.js index db2ee342a4..5e8bb555e2 100644 --- a/packages/rocketchat-lib/client/MessageAction.js +++ b/packages/rocketchat-lib/client/MessageAction.js @@ -180,14 +180,15 @@ Meteor.startup(function() { if (RocketChat.models.Subscriptions.findOne({rid: message.rid}) == null) { return false; } + const deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid); const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); const isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = message.u && message.u._id === Meteor.userId(); - if (!(hasPermission || (isDeleteAllowed && deleteOwn))) { + if (!(hasPermission || (isDeleteAllowed && deleteOwn) || deleteAny)) { return; } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0) { + if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0 && !(deleteAny)) { let msgTs; if (message.ts != null) { msgTs = moment(message.ts); diff --git a/packages/rocketchat-lib/server/methods/deleteMessage.js b/packages/rocketchat-lib/server/methods/deleteMessage.js index 73dd607faf..e6102beab0 100644 --- a/packages/rocketchat-lib/server/methods/deleteMessage.js +++ b/packages/rocketchat-lib/server/methods/deleteMessage.js @@ -23,17 +23,19 @@ Meteor.methods({ action: 'Delete_message' }); } + const deleteAny = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-any-message', originalMessage.rid); + return RocketChat.deleteMessage(originalMessage, Meteor.user()); const hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', originalMessage.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = originalMessage && originalMessage.u && originalMessage.u._id === Meteor.userId(); - if (!(hasPermission || (deleteAllowed && deleteOwn))) { + if (!(hasPermission || (deleteAllowed && deleteOwn)) && !(deleteAny)) { throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { method: 'deleteMessage', action: 'Delete_message' }); } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { + if ((blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) || !(deleteAny)) { if (originalMessage.ts == null) { return; } diff --git a/packages/rocketchat-ui/client/lib/chatMessages.coffee b/packages/rocketchat-ui/client/lib/chatMessages.coffee index 42ddfe9948..1eef99a16d 100644 --- a/packages/rocketchat-ui/client/lib/chatMessages.coffee +++ b/packages/rocketchat-ui/client/lib/chatMessages.coffee @@ -250,8 +250,9 @@ class @ChatMessages $('.sweet-alert').addClass 'visible' deleteMsg: (message) -> + deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid) blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes' - if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 + if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 and deleteAny is false msgTs = moment(message.ts) if message.ts? currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? if currentTsDiff > blockDeleteInMinutes -- GitLab From 97e1d208afcfcc27b7cebf75589f91795e866234 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 09:11:58 -0400 Subject: [PATCH 052/280] fixing spacing --- packages/rocketchat-authorization/server/startup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index a1eae521a1..5cb9cd2c94 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -24,7 +24,7 @@ Meteor.startup(function() { { _id: 'delete-c', roles : ['admin'] }, { _id: 'delete-d', roles : ['admin'] }, { _id: 'delete-message', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'delete-any-message', roles : ['admin', 'owner'] }, + { _id: 'delete-any-message', roles : ['admin', 'owner'] }, { _id: 'delete-p', roles : ['admin'] }, { _id: 'delete-user', roles : ['admin'] }, { _id: 'edit-message', roles : ['admin', 'owner', 'moderator'] }, -- GitLab From 6ebaed71bd17fdbfe0c7f84557aaeaf7a7ce2660 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 09:27:32 -0400 Subject: [PATCH 053/280] undoing modification to shrinkwrap --- .../.npm/package-new-yxxe7j/.gitignore | 1 + .../.npm/package/npm-shrinkwrap.json | 192 +++--------------- 2 files changed, 32 insertions(+), 161 deletions(-) create mode 100644 packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore diff --git a/packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore b/packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore @@ -0,0 +1 @@ +node_modules 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 0b44b80161..c931564c62 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -30,11 +30,6 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "from": "arrify@>=1.0.1 <2.0.0" }, - "ascli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", - "from": "ascli@>=1.0.0 <2.0.0" - }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -65,11 +60,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "from": "aws4@>=1.2.1 <2.0.0" }, - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "from": "balanced-match@>=0.4.1 <0.5.0" - }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", @@ -85,11 +75,6 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "from": "boom@>=2.0.0 <3.0.0" }, - "brace-expansion": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "from": "brace-expansion@>=1.1.7 <2.0.0" - }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -100,16 +85,6 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "from": "buffer-shims@>=1.0.0 <1.1.0" }, - "bytebuffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", - "from": "bytebuffer@>=5.0.0 <6.0.0" - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "from": "camelcase@>=2.0.1 <3.0.0" - }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -125,26 +100,11 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "from": "chalk@>=1.1.1 <2.0.0" }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "from": "cliui@>=3.0.3 <4.0.0" - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "from": "co@>=4.6.0 <5.0.0" }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "from": "code-point-at@>=1.0.0 <2.0.0" - }, - "colour": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", - "from": "colour@>=0.7.1 <0.8.0" - }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -155,11 +115,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "from": "commander@>=2.9.0 <3.0.0" }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "from": "concat-map@0.0.1" - }, "concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", @@ -192,11 +147,6 @@ } } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "from": "decamelize@>=1.1.1 <2.0.0" - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -252,11 +202,6 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", "from": "form-data@>=2.1.1 <2.2.0" }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "from": "fs.realpath@>=1.0.0 <2.0.0" - }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -279,11 +224,6 @@ } } }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "from": "glob@>=7.0.5 <8.0.0" - }, "google-auth-library": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz", @@ -322,8 +262,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.1.tgz", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.0.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -571,8 +511,8 @@ } }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", "from": "extend@>=3.0.0 <3.1.0" }, "forever-agent": { @@ -598,8 +538,8 @@ "from": "har-validator@>=4.2.1 <4.3.0", "dependencies": { "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", "from": "ajv@>=4.9.1 <5.0.0", "dependencies": { "co": { @@ -939,8 +879,8 @@ "from": "tar-pack@>=3.4.0 <4.0.0", "dependencies": { "debug": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.5.tgz", "from": "debug@>=2.2.0 <3.0.0", "dependencies": { "ms": { @@ -963,7 +903,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.1 <2.1.0" + "from": "inherits@>=2.0.0 <2.1.0" } } }, @@ -1006,7 +946,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.0.0 <2.0.0", + "from": "once@>=1.3.3 <2.0.0", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1033,7 +973,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.1 <2.1.0" + "from": "inherits@>=2.0.0 <2.1.0" }, "isarray": { "version": "1.0.0", @@ -1103,31 +1043,16 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "from": "http-signature@>=1.1.0 <1.2.0" }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "from": "inflight@>=1.0.4 <2.0.0" - }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "from": "inherits@>=2.0.3 <3.0.0" }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "from": "invert-kv@>=1.0.0 <2.0.0" - }, "is": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", "from": "is@>=3.0.1 <4.0.0" }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0" - }, "is-my-json-valid": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", @@ -1138,6 +1063,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.0", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.0.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", @@ -1210,11 +1140,6 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", "from": "jws@>=3.1.4 <4.0.0" }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "from": "lcid@>=1.0.0 <2.0.0" - }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -1233,7 +1158,7 @@ "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "from": "long@>=3.0.0 <4.0.0" + "from": "long@>=3.2.0 <4.0.0" }, "methmeth": { "version": "1.1.0", @@ -1255,11 +1180,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "from": "mime-types@>=2.1.7 <2.2.0" }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "from": "minimatch@>=3.0.2 <4.0.0" - }, "modelo": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz", @@ -1275,11 +1195,6 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "from": "node-forge@>=0.7.1 <0.8.0" }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "from": "number-is-nan@>=1.0.0 <2.0.0" - }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -1295,21 +1210,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "from": "once@>=1.3.0 <1.4.0" }, - "optjs": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", - "from": "optjs@>=3.2.2 <3.3.0" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "from": "os-locale@>=1.4.0 <2.0.0" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "from": "path-is-absolute@>=1.0.0 <2.0.0" - }, "performance-now": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", @@ -1336,9 +1236,9 @@ "from": "propprop@>=0.3.1 <0.4.0" }, "protobufjs": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", - "from": "protobufjs@>=5.0.0 <6.0.0" + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.7.3.tgz", + "from": "protobufjs@>=6.7.0 <7.0.0" }, "punycode": { "version": "1.4.1", @@ -1408,19 +1308,14 @@ "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", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.0.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" + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "from": "async@>=1.4.0 <2.0.0" } } }, @@ -1437,8 +1332,8 @@ } }, "stream-events": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.1.tgz", "from": "stream-events@>=1.0.1 <2.0.0" }, "stream-shift": { @@ -1451,11 +1346,6 @@ "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz", "from": "string-format-obj@>=1.1.0 <2.0.0" }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "from": "string-width@>=1.0.1 <2.0.0" - }, "string_decoder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz", @@ -1472,9 +1362,9 @@ "from": "strip-ansi@>=3.0.0 <4.0.0" }, "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "from": "stubs@>=3.0.0 <4.0.0" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-1.1.2.tgz", + "from": "stubs@>=1.1.0 <2.0.0" }, "supports-color": { "version": "2.0.0", @@ -1521,16 +1411,6 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "from": "verror@1.3.6" }, - "window-size": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "from": "window-size@>=0.1.4 <0.2.0" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "from": "wrap-ansi@>=2.0.0 <3.0.0" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1540,16 +1420,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "from": "xtend@>=4.0.0 <5.0.0" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "from": "y18n@>=3.2.0 <4.0.0" - }, - "yargs": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "from": "yargs@>=3.10.0 <4.0.0" } } } -- GitLab From 5c9a8039764c254709933b4da4c74a4046102161 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 09:33:06 -0400 Subject: [PATCH 054/280] Fixing tabs and removing comment --- packages/rocketchat-lib/server/methods/deleteMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-lib/server/methods/deleteMessage.js b/packages/rocketchat-lib/server/methods/deleteMessage.js index e6102beab0..bc77b7330f 100644 --- a/packages/rocketchat-lib/server/methods/deleteMessage.js +++ b/packages/rocketchat-lib/server/methods/deleteMessage.js @@ -23,7 +23,7 @@ Meteor.methods({ action: 'Delete_message' }); } - const deleteAny = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-any-message', originalMessage.rid); + const deleteAny = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-any-message', originalMessage.rid); return RocketChat.deleteMessage(originalMessage, Meteor.user()); const hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', originalMessage.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); -- GitLab From 7301b86c850fa379da12a7dcc6958d86eae9c009 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 09:33:26 -0400 Subject: [PATCH 055/280] Fixing tabs and removing comment --- client/methods/deleteMessage.js | 4 +- .../.npm/package-new-yxxe7j/.gitignore | 1 - .../.npm/package/npm-shrinkwrap.json | 192 +++++++++++++++--- 3 files changed, 162 insertions(+), 35 deletions(-) delete mode 100644 packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore diff --git a/client/methods/deleteMessage.js b/client/methods/deleteMessage.js index deb5789f16..ec7d72a519 100644 --- a/client/methods/deleteMessage.js +++ b/client/methods/deleteMessage.js @@ -10,10 +10,8 @@ Meteor.methods({ //We're now only passed in the `_id` property to lower the amount of data sent to the server message = ChatMessage.findOne({ _id: message._id }); - // should use only hasPermission and change delete-message to array of the - // two desired permissions? const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); - const deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid); + const deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); let deleteOwn = false; diff --git a/packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore b/packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore deleted file mode 100644 index 3c3629e647..0000000000 --- a/packages/rocketchat-google-natural-language/.npm/package-new-yxxe7j/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules 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 c931564c62..0b44b80161 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -30,6 +30,11 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "from": "arrify@>=1.0.1 <2.0.0" }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "from": "ascli@>=1.0.0 <2.0.0" + }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -60,6 +65,11 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "from": "aws4@>=1.2.1 <2.0.0" }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "from": "balanced-match@>=0.4.1 <0.5.0" + }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", @@ -75,6 +85,11 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "from": "boom@>=2.0.0 <3.0.0" }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "from": "brace-expansion@>=1.1.7 <2.0.0" + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -85,6 +100,16 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "from": "buffer-shims@>=1.0.0 <1.1.0" }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "from": "bytebuffer@>=5.0.0 <6.0.0" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "from": "camelcase@>=2.0.1 <3.0.0" + }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -100,11 +125,26 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "from": "chalk@>=1.1.1 <2.0.0" }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "from": "cliui@>=3.0.3 <4.0.0" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "from": "co@>=4.6.0 <5.0.0" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "from": "code-point-at@>=1.0.0 <2.0.0" + }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "from": "colour@>=0.7.1 <0.8.0" + }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -115,6 +155,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "from": "commander@>=2.9.0 <3.0.0" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "from": "concat-map@0.0.1" + }, "concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", @@ -147,6 +192,11 @@ } } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "from": "decamelize@>=1.1.1 <2.0.0" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -202,6 +252,11 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", "from": "form-data@>=2.1.1 <2.2.0" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "from": "fs.realpath@>=1.0.0 <2.0.0" + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -224,6 +279,11 @@ } } }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "from": "glob@>=7.0.5 <8.0.0" + }, "google-auth-library": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz", @@ -262,8 +322,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.0.tgz", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.1.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -511,8 +571,8 @@ } }, "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "from": "extend@>=3.0.0 <3.1.0" }, "forever-agent": { @@ -538,8 +598,8 @@ "from": "har-validator@>=4.2.1 <4.3.0", "dependencies": { "ajv": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "from": "ajv@>=4.9.1 <5.0.0", "dependencies": { "co": { @@ -879,8 +939,8 @@ "from": "tar-pack@>=3.4.0 <4.0.0", "dependencies": { "debug": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.5.tgz", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz", "from": "debug@>=2.2.0 <3.0.0", "dependencies": { "ms": { @@ -903,7 +963,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" } } }, @@ -946,7 +1006,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.3.3 <2.0.0", + "from": "once@>=1.0.0 <2.0.0", "dependencies": { "wrappy": { "version": "1.0.2", @@ -973,7 +1033,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" }, "isarray": { "version": "1.0.0", @@ -1043,16 +1103,31 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "from": "http-signature@>=1.1.0 <1.2.0" }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "from": "inflight@>=1.0.4 <2.0.0" + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "from": "inherits@>=2.0.3 <3.0.0" }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "from": "invert-kv@>=1.0.0 <2.0.0" + }, "is": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", "from": "is@>=3.0.1 <4.0.0" }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0" + }, "is-my-json-valid": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", @@ -1063,11 +1138,6 @@ "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.0", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.0.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", @@ -1140,6 +1210,11 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", "from": "jws@>=3.1.4 <4.0.0" }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "from": "lcid@>=1.0.0 <2.0.0" + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -1158,7 +1233,7 @@ "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "from": "long@>=3.2.0 <4.0.0" + "from": "long@>=3.0.0 <4.0.0" }, "methmeth": { "version": "1.1.0", @@ -1180,6 +1255,11 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "from": "mime-types@>=2.1.7 <2.2.0" }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "from": "minimatch@>=3.0.2 <4.0.0" + }, "modelo": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz", @@ -1195,6 +1275,11 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "from": "node-forge@>=0.7.1 <0.8.0" }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "from": "number-is-nan@>=1.0.0 <2.0.0" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -1210,6 +1295,21 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "from": "once@>=1.3.0 <1.4.0" }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "from": "optjs@>=3.2.2 <3.3.0" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "from": "os-locale@>=1.4.0 <2.0.0" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "from": "path-is-absolute@>=1.0.0 <2.0.0" + }, "performance-now": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", @@ -1236,9 +1336,9 @@ "from": "propprop@>=0.3.1 <0.4.0" }, "protobufjs": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.7.3.tgz", - "from": "protobufjs@>=6.7.0 <7.0.0" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", + "from": "protobufjs@>=5.0.0 <6.0.0" }, "punycode": { "version": "1.4.1", @@ -1308,14 +1408,19 @@ "from": "sntp@>=1.0.0 <2.0.0" }, "split-array-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.0.tgz", + "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": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "from": "async@>=1.4.0 <2.0.0" + "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" } } }, @@ -1332,8 +1437,8 @@ } }, "stream-events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.1.tgz", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", "from": "stream-events@>=1.0.1 <2.0.0" }, "stream-shift": { @@ -1346,6 +1451,11 @@ "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz", "from": "string-format-obj@>=1.1.0 <2.0.0" }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "from": "string-width@>=1.0.1 <2.0.0" + }, "string_decoder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz", @@ -1362,9 +1472,9 @@ "from": "strip-ansi@>=3.0.0 <4.0.0" }, "stubs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-1.1.2.tgz", - "from": "stubs@>=1.1.0 <2.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "from": "stubs@>=3.0.0 <4.0.0" }, "supports-color": { "version": "2.0.0", @@ -1411,6 +1521,16 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "from": "verror@1.3.6" }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "from": "window-size@>=0.1.4 <0.2.0" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "from": "wrap-ansi@>=2.0.0 <3.0.0" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1420,6 +1540,16 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "from": "xtend@>=4.0.0 <5.0.0" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "from": "y18n@>=3.2.0 <4.0.0" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "from": "yargs@>=3.10.0 <4.0.0" } } } -- GitLab From 64104549be5836d579229d87980d311386e96295 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 09:40:05 -0400 Subject: [PATCH 056/280] Removing line from debug testing --- packages/rocketchat-lib/server/methods/deleteMessage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rocketchat-lib/server/methods/deleteMessage.js b/packages/rocketchat-lib/server/methods/deleteMessage.js index bc77b7330f..8717b1fc8c 100644 --- a/packages/rocketchat-lib/server/methods/deleteMessage.js +++ b/packages/rocketchat-lib/server/methods/deleteMessage.js @@ -24,7 +24,6 @@ Meteor.methods({ }); } const deleteAny = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-any-message', originalMessage.rid); - return RocketChat.deleteMessage(originalMessage, Meteor.user()); const hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', originalMessage.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = originalMessage && originalMessage.u && originalMessage.u._id === Meteor.userId(); -- GitLab From adeef9901bc3c620de6c4808bd03bd57a2f294c5 Mon Sep 17 00:00:00 2001 From: bbrauns Date: Tue, 9 May 2017 16:13:09 +0200 Subject: [PATCH 057/280] [New] LDAP: Use variables in User_Data_FieldMap for name mapping --- packages/rocketchat-ldap/server/sync.js | 48 ++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index df3ec4bf82..86d7faf44f 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -66,16 +66,18 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { const fieldMap = JSON.parse(syncUserDataFieldMap); const userData = {}; - const emailList = []; - _.map(fieldMap, function(userField, ldapField) { - if (!ldapUser.object.hasOwnProperty(ldapField)) { - return; - } + const emailList = []; + _.map(fieldMap, function (userField, ldapField) { 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) { + _.map(ldapUser.object[ldapField], function (item) { emailList.push({ address: item, verified: true }); }); } else { @@ -84,8 +86,38 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { break; case 'name': - if (user.name !== ldapUser.object[ldapField]) { - userData.name = ldapUser.object[ldapField]; + var templateRegex = /#{(\w+)}/gi; + var match = templateRegex.exec(ldapField) + var 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) { + var tmplVar = match[0]; + var tmplAttrName = match[1]; + + if (!ldapUser.object.hasOwnProperty(tmplAttrName)) { + logger.debug('user does not have attribute: ' + tmplAttrName) + return; + } + + var 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; } -- GitLab From 391f98b3b690b0ee2048ab80fa2ca0c35d8a2d54 Mon Sep 17 00:00:00 2001 From: bbrauns Date: Tue, 9 May 2017 16:43:21 +0200 Subject: [PATCH 058/280] refactor: missing semicolons, template syntax for string concat --- packages/rocketchat-ldap/server/sync.js | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index 86d7faf44f..7fb1517f25 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -68,16 +68,16 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { const emailList = []; - _.map(fieldMap, function (userField, ldapField) { + _.map(fieldMap, function(userField, ldapField) { switch (userField) { case 'email': if (!ldapUser.object.hasOwnProperty(ldapField)) { - logger.debug('user does not have attribute: ' + ldapField) + logger.debug(`user does not have attribute: ${ldapField}`); return; } if (_.isObject(ldapUser.object[ldapField])) { - _.map(ldapUser.object[ldapField], function (item) { + _.map(ldapUser.object[ldapField], function(item) { emailList.push({ address: item, verified: true }); }); } else { @@ -86,38 +86,37 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { break; case 'name': - var templateRegex = /#{(\w+)}/gi; - var match = templateRegex.exec(ldapField) - var tmpLdapField = 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) + logger.debug(`user does not have attribute: ${ldapField}`) return; } tmpLdapField = ldapUser.object[ldapField]; - } - else { - logger.debug('template found. replacing values') + } else { + logger.debug('template found. replacing values'); while (match != null) { - var tmplVar = match[0]; - var tmplAttrName = match[1]; + const tmplVar = match[0]; + const tmplAttrName = match[1]; if (!ldapUser.object.hasOwnProperty(tmplAttrName)) { - logger.debug('user does not have attribute: ' + tmplAttrName) + logger.debug(`user does not have attribute: ${tmplAttrName}`); return; } - var attrVal = ldapUser.object[tmplAttrName]; - logger.debug('replacing template var: ' + tmplVar + ' with value from ldap: ' + attrVal); + 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) + match = templateRegex.exec(ldapField); } } if (user.name !== tmpLdapField) { userData.name = tmpLdapField; - logger.debug('user.name changed to: ' + tmpLdapField); + logger.debug(`user.name changed to: ${tmpLdapField}`); } break; } -- GitLab From 666a4acf022bc6a36021a6e4f7137f20894d1d67 Mon Sep 17 00:00:00 2001 From: bbrauns Date: Tue, 9 May 2017 16:51:38 +0200 Subject: [PATCH 059/280] refactor: spaces in string templates added; missing semicolon --- packages/rocketchat-ldap/server/sync.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index 7fb1517f25..eb7a27db3f 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -65,8 +65,6 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { if (syncUserData && syncUserDataFieldMap) { const fieldMap = JSON.parse(syncUserDataFieldMap); const userData = {}; - - const emailList = []; _.map(fieldMap, function(userField, ldapField) { switch (userField) { @@ -87,12 +85,12 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { case 'name': const templateRegex = /#{(\w+)}/gi; - let match = templateRegex.exec(ldapField) + let match = templateRegex.exec(ldapField); let tmpLdapField = ldapField; if (match == null) { if (!ldapUser.object.hasOwnProperty(ldapField)) { - logger.debug(`user does not have attribute: ${ldapField}`) + logger.debug(`user does not have attribute: ${ldapField}`); return; } tmpLdapField = ldapUser.object[ldapField]; -- GitLab From df7a55da19b8e5e03be400b07722302183c99f0b Mon Sep 17 00:00:00 2001 From: bbrauns Date: Tue, 9 May 2017 17:11:08 +0200 Subject: [PATCH 060/280] refactor: spaces in string templates added --- packages/rocketchat-ldap/server/sync.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index eb7a27db3f..06763c0a97 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -70,7 +70,7 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { switch (userField) { case 'email': if (!ldapUser.object.hasOwnProperty(ldapField)) { - logger.debug(`user does not have attribute: ${ldapField}`); + logger.debug(`user does not have attribute: ${ ldapField }`); return; } @@ -90,7 +90,7 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { if (match == null) { if (!ldapUser.object.hasOwnProperty(ldapField)) { - logger.debug(`user does not have attribute: ${ldapField}`); + logger.debug(`user does not have attribute: ${ ldapField }`); return; } tmpLdapField = ldapUser.object[ldapField]; @@ -101,12 +101,12 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { const tmplAttrName = match[1]; if (!ldapUser.object.hasOwnProperty(tmplAttrName)) { - logger.debug(`user does not have attribute: ${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}`); + logger.debug(`replacing template var: ${ tmplVar } with value from ldap: ${ attrVal }`); tmpLdapField = tmpLdapField.replace(tmplVar, attrVal); match = templateRegex.exec(ldapField); } @@ -114,7 +114,7 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { if (user.name !== tmpLdapField) { userData.name = tmpLdapField; - logger.debug(`user.name changed to: ${tmpLdapField}`); + logger.debug(`user.name changed to: ${ tmpLdapField }`); } break; } -- GitLab From 9cf1bcc079e8572d29562cae369134c33b6be5d4 Mon Sep 17 00:00:00 2001 From: Philip Hutchins Date: Tue, 9 May 2017 11:11:34 -0400 Subject: [PATCH 061/280] Changing name of delete-any-message to force-delete-message --- client/methods/deleteMessage.js | 6 +++--- packages/rocketchat-authorization/server/startup.js | 2 +- packages/rocketchat-lib/client/MessageAction.js | 6 +++--- packages/rocketchat-lib/server/methods/deleteMessage.js | 6 +++--- packages/rocketchat-ui/client/lib/chatMessages.coffee | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client/methods/deleteMessage.js b/client/methods/deleteMessage.js index ec7d72a519..8a86f3affc 100644 --- a/client/methods/deleteMessage.js +++ b/client/methods/deleteMessage.js @@ -11,18 +11,18 @@ Meteor.methods({ message = ChatMessage.findOne({ _id: message._id }); const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); - const deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid); + const forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); let deleteOwn = false; if (message && message.u && message.u._id) { deleteOwn = message.u._id === Meteor.userId(); } - if (!(deleteAny || hasPermission || (deleteAllowed && deleteOwn))) { + if (!(forceDelete || hasPermission || (deleteAllowed && deleteOwn))) { return false; } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if (!(deleteAny) || (_.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0)) { + if (!(forceDelete) || (_.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0)) { if (message.ts) { const msgTs = moment(message.ts); if (msgTs) { diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 5cb9cd2c94..3fe7771125 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -24,7 +24,6 @@ Meteor.startup(function() { { _id: 'delete-c', roles : ['admin'] }, { _id: 'delete-d', roles : ['admin'] }, { _id: 'delete-message', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'delete-any-message', roles : ['admin', 'owner'] }, { _id: 'delete-p', roles : ['admin'] }, { _id: 'delete-user', roles : ['admin'] }, { _id: 'edit-message', roles : ['admin', 'owner', 'moderator'] }, @@ -33,6 +32,7 @@ Meteor.startup(function() { { _id: 'edit-other-user-password', roles : ['admin'] }, { _id: 'edit-privileged-setting', roles : ['admin'] }, { _id: 'edit-room', roles : ['admin', 'owner', 'moderator'] }, + { _id: 'force-delete-message', roles : ['admin', 'owner'] }, { _id: 'join-without-join-code', roles : ['admin', 'bot'] }, { _id: 'manage-assets', roles : ['admin'] }, { _id: 'manage-emoji', roles : ['admin'] }, diff --git a/packages/rocketchat-lib/client/MessageAction.js b/packages/rocketchat-lib/client/MessageAction.js index 5e8bb555e2..51ad67e3fa 100644 --- a/packages/rocketchat-lib/client/MessageAction.js +++ b/packages/rocketchat-lib/client/MessageAction.js @@ -180,15 +180,15 @@ Meteor.startup(function() { if (RocketChat.models.Subscriptions.findOne({rid: message.rid}) == null) { return false; } - const deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid); + const forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid); const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); const isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = message.u && message.u._id === Meteor.userId(); - if (!(hasPermission || (isDeleteAllowed && deleteOwn) || deleteAny)) { + if (!(hasPermission || (isDeleteAllowed && deleteOwn) || forceDelete)) { return; } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0 && !(deleteAny)) { + if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0 && !(forceDelete)) { let msgTs; if (message.ts != null) { msgTs = moment(message.ts); diff --git a/packages/rocketchat-lib/server/methods/deleteMessage.js b/packages/rocketchat-lib/server/methods/deleteMessage.js index 8717b1fc8c..f6954129df 100644 --- a/packages/rocketchat-lib/server/methods/deleteMessage.js +++ b/packages/rocketchat-lib/server/methods/deleteMessage.js @@ -23,18 +23,18 @@ Meteor.methods({ action: 'Delete_message' }); } - const deleteAny = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-any-message', originalMessage.rid); + const forceDelete = RocketChat.authz.hasPermission(Meteor.userId(), 'force-delete-message', originalMessage.rid); const hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', originalMessage.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = originalMessage && originalMessage.u && originalMessage.u._id === Meteor.userId(); - if (!(hasPermission || (deleteAllowed && deleteOwn)) && !(deleteAny)) { + if (!(hasPermission || (deleteAllowed && deleteOwn)) && !(forceDelete)) { throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { method: 'deleteMessage', action: 'Delete_message' }); } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if ((blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) || !(deleteAny)) { + if ((blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) || !(forceDelete)) { if (originalMessage.ts == null) { return; } diff --git a/packages/rocketchat-ui/client/lib/chatMessages.coffee b/packages/rocketchat-ui/client/lib/chatMessages.coffee index 1eef99a16d..bdeb2d72ac 100644 --- a/packages/rocketchat-ui/client/lib/chatMessages.coffee +++ b/packages/rocketchat-ui/client/lib/chatMessages.coffee @@ -250,9 +250,9 @@ class @ChatMessages $('.sweet-alert').addClass 'visible' deleteMsg: (message) -> - deleteAny = RocketChat.authz.hasAtLeastOnePermission('delete-any-message', message.rid) + forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid) blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes' - if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 and deleteAny is false + if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 and forceDelete is false msgTs = moment(message.ts) if message.ts? currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? if currentTsDiff > blockDeleteInMinutes -- GitLab From d0d880470d1aa924b7681310d20171a168308cd5 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 9 May 2017 14:09:12 -0300 Subject: [PATCH 062/280] fix review --- packages/rocketchat-ui-admin/client/admin.js | 157 +++++-------------- 1 file changed, 38 insertions(+), 119 deletions(-) diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js index 56f59ba917..63cdb82e1c 100644 --- a/packages/rocketchat-ui-admin/client/admin.js +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -74,14 +74,12 @@ Template.admin.onDestroyed(function() { Template.admin.helpers({ languages() { const languages = TAPi18n.getLanguages(); - let result = []; - Object.keys(languages).forEach((key) => { - const language = languages[key]; - result.push(_.extend(language, { - key - })); + let result = Object.keys(languages).forEach(key => { + const language = languages[key]; + return _.extend(language, { key }); }); + result = _.sortBy(result, 'key'); result.unshift({ 'name': 'Default', @@ -103,17 +101,10 @@ Template.admin.helpers({ if (!group) { return; } - const settings = RocketChat.settings.collectionPrivate.find({ - group: groupId - }, { - sort: { - section: 1, - sorter: 1, - i18nLabel: 1 - } - }).fetch(); + const settings = RocketChat.settings.collectionPrivate.find({ group: groupId }, { sort: { section: 1, sorter: 1, i18nLabel: 1 }}).fetch(); const sections = {}; - Object.keys(settings).forEach((key) => { + + Object.keys(settings).forEach(key => { const setting = settings[key]; if (setting.i18nDefaultQuery != null) { if (_.isString(setting.i18nDefaultQuery)) { @@ -124,7 +115,7 @@ Template.admin.helpers({ if (!_.isArray(i18nDefaultQuery)) { i18nDefaultQuery = [i18nDefaultQuery]; } - Object.keys(i18nDefaultQuery).forEach((key) => { + Object.keys(i18nDefaultQuery).forEach(key => { const item = i18nDefaultQuery[key]; if (RocketChat.settings.collectionPrivate.findOne(item) != null) { setting.value = TAPi18n.__(`${ setting._id }_Default`); @@ -138,13 +129,12 @@ Template.admin.helpers({ sections[settingSection].push(setting); }); - group.sections = []; - Object.keys(sections).forEach((key) =>{ + group.sections = Object.keys(sections).map(key =>{ const value = sections[key]; - group.sections.push({ + return { section: key, settings: value - }); + }; }); return group; }, @@ -171,7 +161,7 @@ Template.admin.helpers({ } let found = 0; - Object.keys(enableQuery).forEach((key) =>{ + Object.keys(enableQuery).forEach(key =>{ const item = enableQuery[key]; if (TempSettings.findOne(item) != null) { found++; @@ -262,10 +252,7 @@ Template.admin.helpers({ random() { return Random.id(); }, - getEditorOptions(readOnly) { - if (readOnly == null) { - readOnly = false; - } + getEditorOptions(readOnly = false) { return { lineNumbers: true, mode: this.code || 'javascript', @@ -312,8 +299,10 @@ Template.admin.helpers({ autocompleteRoom() { return { limit: 10, + //inputDelay: 300 rules: [ { + //@TODO maybe change this 'collection' and/or template collection: 'CachedChannelList', subscription: 'channelAndPrivateAutocomplete', field: 'name', @@ -337,22 +326,14 @@ Template.admin.helpers({ return color.replace(/theme-color-/, '@'); }, showResetButton() { - const setting = TempSettings.findOne({ - _id: this._id - }, { - fields: { - value: 1, - packageValue: 1 - } - }); + const setting = TempSettings.findOne({ _id: this._id }, { fields: { value: 1, packageValue: 1 }}); return this.type !== 'asset' && setting.value !== setting.packageValue && !this.blocked; } }); Template.admin.events({ 'change .input-monitor, keyup .input-monitor': _.throttle(function(e) { - let value; - value = _.trim($(e.target).val()); + let value = _.trim($(e.target).val()); switch (this.type) { case 'int': value = parseInt(value); @@ -371,13 +352,7 @@ Template.admin.events({ }, 500), 'change select[name=color-editor]'(e) { const value = _.trim($(e.target).val()); - return TempSettings.update({ - _id: this._id - }, { - $set: { - editor: value - } - }); + return TempSettings.update({ _id: this._id }, { $set: { editor: value }}); }, 'click .submit .discard'() { const group = FlowRouter.getParam('group'); @@ -386,22 +361,9 @@ Template.admin.events({ changed: true }; const settings = TempSettings.find(query, { - fields: { - _id: 1, - value: 1, - packageValue: 1 - } - }).fetch(); + fields: { _id: 1, value: 1, packageValue: 1 }}).fetch(); return settings.forEach(function(setting) { - const oldSetting = RocketChat.settings.collectionPrivate.findOne({ - _id: setting._id - }, { - fields: { - value: 1, - type: 1, - editor: 1 - } - }); + const oldSetting = RocketChat.settings.collectionPrivate.findOne({ _id: setting._id }, { fields: { value: 1, type: 1, editor: 1 }}); return setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor); }); }, @@ -420,32 +382,14 @@ Template.admin.events({ const group = FlowRouter.getParam('group'); const section = $(e.target).data('section'); if (section === '') { - settings = TempSettings.find({ - group, - section: { - $exists: false - } - }, { - fields: { - _id: 1 - } - }).fetch(); + settings = TempSettings.find({ group, section: { $exists: false }}, { fields: { _id: 1 }}).fetch(); } else { - settings = TempSettings.find({ - group, - section - }, { - fields: { - _id: 1 - } - }).fetch(); + settings = TempSettings.find({ group, section }, { fields: { _id: 1 }}).fetch(); } return settings.forEach(function(setting) { const defaultValue = getDefaultSetting(setting._id); setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor); - return TempSettings.update({ - _id: setting._id - }, { + return TempSettings.update({_id: setting._id }, { $set: { value: defaultValue.packageValue, changed: RocketChat.settings.collectionPrivate.findOne(setting._id).value !== defaultValue.packageValue @@ -455,29 +399,14 @@ Template.admin.events({ }, 'click .submit .save'() { const group = FlowRouter.getParam('group'); - const query = { - group, - changed: true - }; - const settings = TempSettings.find(query, { - fields: { - _id: 1, - value: 1, - editor: 1 - } - }).fetch(); + const query = { group, changed: true }; + const settings = TempSettings.find(query, { fields: { _id: 1, value: 1, editor: 1 }}).fetch(); if (!_.isEmpty(settings)) { return RocketChat.settings.batchSet(settings, function(err) { if (err) { return handleError(err); } - TempSettings.update({ - changed: true - }, { - $unset: { - changed: 1 - } - }); + TempSettings.update({ changed: true }, { $unset: { changed: 1 }}); return toastr.success(TAPi18n.__('Settings_updated')); }); } @@ -550,22 +479,20 @@ Template.admin.events({ } } const results = []; - Object.keys(files).forEach((key) => { + Object.keys(files).forEach(key => { const blob = files[key]; toastr.info(TAPi18n.__('Uploading_file')); const reader = new FileReader(); reader.readAsBinaryString(blob); results.push(reader.onloadend = () => { - return function() { - return Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) { - if (err != null) { - handleError(err); - console.log(err); - return; - } - return toastr.success(TAPi18n.__('File_uploaded')); - }); - }; + return Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) { + if (err != null) { + handleError(err); + console.log(err); + return; + } + return toastr.success(TAPi18n.__('File_uploaded')); + }); }); }); return results; @@ -612,9 +539,7 @@ Template.admin.events({ selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc); instance.selectedRooms.set(selectedRooms); const value = selectedRooms[this.id]; - TempSettings.update({ - _id: this.id - }, { + TempSettings.update({ _id: this.id }, { $set: { value, changed: RocketChat.settings.collectionPrivate.findOne(this.id).value !== value @@ -632,9 +557,7 @@ Template.admin.events({ }); instance.selectedRooms.set(selectedRooms); const value = selectedRooms[settingId]; - return TempSettings.update({ - _id: settingId - }, { + return TempSettings.update({ _id: settingId }, { $set: { value, changed: RocketChat.settings.collectionPrivate.findOne(settingId).value !== value @@ -652,11 +575,7 @@ Template.admin.onRendered(function() { const hasColor = TempSettings.findOne({ group: FlowRouter.getParam('group'), type: 'color' - }, { - fields: { - _id: 1 - } - }); + }, { fields: { _id: 1 }}); if (hasColor) { return Meteor.setTimeout(function() { return $('.colorpicker-input').each(function(index, el) { -- GitLab From 178611cb957e40744b9b8365345b4a95462a423d Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 9 May 2017 14:21:21 -0300 Subject: [PATCH 063/280] update admin.js --- packages/rocketchat-ui-admin/client/admin.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js index 63cdb82e1c..3dd0141bdd 100644 --- a/packages/rocketchat-ui-admin/client/admin.js +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -478,13 +478,13 @@ Template.admin.events({ files = []; } } - const results = []; + Object.keys(files).forEach(key => { const blob = files[key]; toastr.info(TAPi18n.__('Uploading_file')); const reader = new FileReader(); reader.readAsBinaryString(blob); - results.push(reader.onloadend = () => { + reader.onloadend = () => { return Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) { if (err != null) { handleError(err); @@ -493,9 +493,8 @@ Template.admin.events({ } return toastr.success(TAPi18n.__('File_uploaded')); }); - }); + }; }); - return results; }, 'click .expand'(e) { $(e.currentTarget).closest('.section').removeClass('section-collapsed'); -- GitLab From dfe09f9614d9b10e4987341608b466a89a740448 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 9 May 2017 16:21:30 -0300 Subject: [PATCH 064/280] convert autocomplete-client to js and add comments --- .../client/autocomplete-client.js | 280 ++++++++++-------- packages/meteor-autocomplete/package.js | 2 +- 2 files changed, 163 insertions(+), 119 deletions(-) diff --git a/packages/meteor-autocomplete/client/autocomplete-client.js b/packages/meteor-autocomplete/client/autocomplete-client.js index 6c2f9d902b..7f11465e1e 100755 --- a/packages/meteor-autocomplete/client/autocomplete-client.js +++ b/packages/meteor-autocomplete/client/autocomplete-client.js @@ -1,4 +1,4 @@ -/* globals Deps */ +/* globals Deps, getCaretCoordinates*/ const AutoCompleteRecords = new Mongo.Collection('autocompleteRecords'); const isServerSearch = function(rule) { @@ -9,41 +9,51 @@ 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 != null) { return 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 }; } @@ -72,48 +82,59 @@ this.AutoComplete = class { validateRule(rule); }); - this.expressions = (function() { - const results = []; - Object.keys(rules).forEach((key) => { + this.expressions = (() => { + return Object.keys(rules).map((key) => { const rule = rules[key]; - results.push(getRegExp(rule)); + return getRegExp(rule); }); - return results; - }); + })(); 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((function(_this) { - return function() { - let filter, options, ref1, ref2, selector, subName; - if ((ref1 = _this.sub) != null) { - ref1.stop(); - } - if (!((rule = _this.matchedRule()) && (filter = _this.getFilter()) !== null)) { - return; - } - if (!isServerSearch(rule)) { - _this.setLoaded(true); - return; - } - ref2 = getFindParams(rule, filter, _this.limit), selector = ref2[0], options = ref2[1]; - _this.setLoaded(false); - subName = rule.subscription || 'autocomplete-recordset'; - return _this.sub = Meteor.subscribe(subName, selector, options, rule.collection, function() { - return _this.setLoaded(true); - }); - }; - })(this)); + 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 return 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]; @@ -124,7 +145,7 @@ this.AutoComplete = class { setMatchedRule(i) { this.matched = i; - return this.ruleDep.changed(); + this.ruleDep.changed(); } getFilter() { @@ -145,38 +166,42 @@ this.AutoComplete = class { setLoaded(val) { if (val === this.loaded) { - return; + return; //Don't cause redraws unnecessarily } this.loaded = val; return this.loadingDep.changed(); } onKeyUp() { - let breakLoop, i, matches, results, startpos, val; if (!this.$element) { - return; + return; //Don't try to do this while loading } - startpos = this.element.selectionStart; - val = this.getText().substring(0, startpos); + 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. */ - i = 0; - breakLoop = false; - results = []; + let i = 0; + let breakLoop = false; while (i < this.expressions.length) { - matches = val.match(this.expressions[i]); + 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; @@ -184,54 +209,54 @@ this.AutoComplete = class { if (breakLoop) { break; } - results.push(i++); + i++; } - return results; } onKeyDown(e) { - if (this.matched === -1 || (this.constructor.KEYS.indexOf(e.keyCode) < 0)) { + if (this.matched === -1 || (this.KEYS.indexOf(e.keyCode) < 0)) { return; } switch (e.keyCode) { - case 9: - case 13: - if (this.select()) { + case 9: //TAB + case 13: //ENTER + if (this.select()) { //Don't jump fields or submit if select successful e.preventDefault(); e.stopPropagation(); } break; - case 40: + // preventDefault needed below to avoid moving cursor when selecting + case 40: //DOWN e.preventDefault(); this.next(); break; - case 38: + case 38: //UP e.preventDefault(); this.prev(); break; - case 27: + case 27: //ESCAPE this.$element.blur(); this.hideList(); } } onFocus() { - return Meteor.defer((function(_this) { - return function() { - return _this.onKeyUp(); - }; - })(this)); + // We need to run onKeyUp after the focus resolves, + // or the caret position (selectionStart) will not be correct + return Meteor.defer(() => { + return this.onKeyUp(); + }); } onBlur() { - return Meteor.setTimeout((function(_this) { - return function() { - return _this.hideList(); - }; - }(this)), 500); + // 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(() => { + return this.hideList(); + }, 500); } - onItemClick(doc, e) { + onItemClick(doc) { return this.processSelection(doc, this.rules[this.matched]); } @@ -241,80 +266,94 @@ this.AutoComplete = class { } filteredList() { - let filter, options, ref, rule, selector; - filter = this.getFilter(); + // @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; } - rule = this.rules[this.matched]; + 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; } - ref = getFindParams(rule, filter, this.limit), selector = ref[0], options = ref[1]; - Meteor.defer((function(_this) { - return function() { - return _this.ensureSelection(); - }; - })(this)); + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + Meteor.defer(() => { + return 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() { - let rule, showing; - rule = this.matchedRule(); - showing = (rule != null) && (rule.token || this.getFilter()); + const rule = this.matchedRule(); + // Same rules as above + const showing = rule && (rule.token || this.getFilter()); + + // Do this after the render if (showing) { - Meteor.defer((function(_this) { - return function() { - _this.positionContainer(); - return _this.ensureSelection(); - }; - })(this)); + Meteor.defer(() => { + this.positionContainer(); + return this.ensureSelection(); + }); } return showing; } + // Replace text with currently selected item select() { - let doc, node; - node = this.tmplInst.find('.-autocomplete-item.selected'); + const node = this.tmplInst.find('.-autocomplete-item.selected'); if (node == null) { return false; } - doc = Blaze.getData(node); + const doc = Blaze.getData(node); if (!doc) { - return false; + return false; //Don't select if nothing matched + } this.processSelection(doc, this.rules[this.matched]); return true; } processSelection(doc, rule) { - let replacement; - replacement = getField(doc, rule.field); + 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) { - let finalFight, fullStuff, newPosition, posfix, separator, startpos, val; - startpos = this.element.selectionStart; - fullStuff = this.getText(); - val = fullStuff.substring(0, startpos); + 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 }`); - posfix = fullStuff.substring(startpos, fullStuff.length); - separator = (posfix.match(/^\s/) ? '' : ' '); - finalFight = val + separator + posfix; + const posfix = fullStuff.substring(startpos, fullStuff.length); + const separator = (posfix.match(/^\s/) ? '' : ' '); + const finalFight = val + separator + posfix; this.setText(finalFight); - newPosition = val.length + 1; + const newPosition = val.length + 1; this.element.setSelectionRange(newPosition, newPosition); } @@ -341,20 +380,24 @@ this.AutoComplete = class { */ positionContainer() { - let offset, pos, position, rule; - position = this.$element.position(); - rule = this.matchedRule(); - offset = getCaretCoordinates(this.element, this.element.selectionStart); - if ((rule != null) && isWholeField(rule)) { + // 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() - }; - } else { - pos = { - left: position.left + offset.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 { @@ -364,53 +407,54 @@ this.AutoComplete = class { } ensureSelection() { - let selectedItem; - selectedItem = this.tmplInst.$('.-autocomplete-item.selected'); + // 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 return this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); } } + // Select next item in list next() { - let currentItem, next; - currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); if (!currentItem.length) { return; } currentItem.removeClass('selected'); - next = currentItem.next(); + const next = currentItem.next(); if (next.length) { return next.addClass('selected'); - } else { + } else { //End of list or lost selection; Go back to first item return this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); } } + //Select previous item in list prev() { - let currentItem, prev; - currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); if (!currentItem.length) { - return; + return; //Don't try to iterate an empty list } currentItem.removeClass('selected'); - prev = currentItem.prev(); + const prev = currentItem.prev(); if (prev.length) { return prev.addClass('selected'); - } else { + } else { //Beginning of list or lost selection; Go to end of list return 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; } - - }; -const AutocompleteTest = { - records: AutoCompleteRecords, - getRegExp, - getFindParams -}; +// AutocompleteTest = { +// records: AutoCompleteRecords, +// getRegExp, +// getFindParams +// }; diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js index 8694c20acc..e5da193ba0 100755 --- a/packages/meteor-autocomplete/package.js +++ b/packages/meteor-autocomplete/package.js @@ -16,7 +16,7 @@ Package.onUse(function(api) { api.addFiles([ 'client/autocomplete.css', 'client/inputs.html', - 'client/autocomplete-client.coffee', + 'client/autocomplete-client.js', 'client/templates.coffee' ], 'client'); -- GitLab From 29452fee5adc6130fc84fa325ba797418d29cd1b Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 10 May 2017 11:03:16 -0300 Subject: [PATCH 065/280] fix .map --- packages/rocketchat-ui-admin/client/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js index 3dd0141bdd..4db8056911 100644 --- a/packages/rocketchat-ui-admin/client/admin.js +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -75,7 +75,7 @@ Template.admin.helpers({ languages() { const languages = TAPi18n.getLanguages(); - let result = Object.keys(languages).forEach(key => { + let result = Object.keys(languages).map(key => { const language = languages[key]; return _.extend(language, { key }); }); -- GitLab From 0448acba3550385eb72fb4db467e8efc8b4b4378 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 10 May 2017 12:47:22 -0300 Subject: [PATCH 066/280] fix Sam's review --- packages/rocketchat-mentions/Mentions.js | 1 - .../rocketchat-mentions/tests/client.tests.js | 17 +++--- .../rocketchat-mentions/tests/server.tests.js | 60 ++++++++++--------- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/packages/rocketchat-mentions/Mentions.js b/packages/rocketchat-mentions/Mentions.js index 0f4f2dada9..1608252488 100644 --- a/packages/rocketchat-mentions/Mentions.js +++ b/packages/rocketchat-mentions/Mentions.js @@ -27,7 +27,6 @@ export default class { return new RegExp(`#(${ this.pattern })`, 'gm'); } replaceUsers(str, message, me) { - console.log(message.mentions); return str.replace(this.userMentionRegex, (match, username) => { if (['all', 'here'].includes(username)) { return `${ match }`; diff --git a/packages/rocketchat-mentions/tests/client.tests.js b/packages/rocketchat-mentions/tests/client.tests.js index e6adff0c06..b7cacc0d77 100644 --- a/packages/rocketchat-mentions/tests/client.tests.js +++ b/packages/rocketchat-mentions/tests/client.tests.js @@ -2,22 +2,23 @@ import assert from 'assert'; import Mentions from '../Mentions'; -const mention = new Mentions({ - pattern: '[0-9a-zA-Z-_.]+', - me: () => 'me' +let mention; +beforeEach(function functionName() { + mention = new Mentions({ + pattern: '[0-9a-zA-Z-_.]+', + me: () => 'me' + }); }); - describe('Mention', function() { describe('get pattern', () => { const regexp = '[0-9a-zA-Z-_.]+'; + beforeEach(() => mention.pattern = () => regexp); describe('by function', function functionName() { - before(() => mention.pattern = () => regexp); it(`should be equal to ${ regexp }`, ()=> { assert.equal(regexp, mention.pattern); }); }); describe('by const', function functionName() { - before(() => mention.pattern = regexp); it(`should be equal to ${ regexp }`, ()=> { assert.equal(regexp, mention.pattern); }); @@ -26,13 +27,13 @@ describe('Mention', function() { describe('get me', () => { const me = 'me'; describe('by function', function functionName() { - before(() => mention.me = () => me); + beforeEach(() => mention.me = () => me); it(`should be equal to ${ me }`, ()=> { assert.equal(me, mention.me); }); }); describe('by const', function functionName() { - before(() => mention.me = me); + beforeEach(() => mention.me = me); it(`should be equal to ${ me }`, ()=> { assert.equal(me, mention.me); }); diff --git a/packages/rocketchat-mentions/tests/server.tests.js b/packages/rocketchat-mentions/tests/server.tests.js index aa579b3811..b5fea3dc27 100644 --- a/packages/rocketchat-mentions/tests/server.tests.js +++ b/packages/rocketchat-mentions/tests/server.tests.js @@ -4,43 +4,47 @@ import assert from 'assert'; import MentionsServer from '../MentionsServer'; -const mention = new MentionsServer({ - pattern: '[0-9a-zA-Z-_.]+', - messageMaxAll: () => 4, //|| RocketChat.settings.get('Message_MaxAll') - getUsers: (usernames) => { - return [{ - _id: 1, - username: 'rocket.cat' - }, { - _id: 2, - username: 'jon' - }].filter(user => usernames.includes(user.username));//Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); - }, - getChannel: () => { - return { - usernames: [{ +let mention; + +beforeEach(function() { + mention = new MentionsServer({ + pattern: '[0-9a-zA-Z-_.]+', + messageMaxAll: () => 4, //|| RocketChat.settings.get('Message_MaxAll') + getUsers: (usernames) => { + return [{ _id: 1, username: 'rocket.cat' }, { _id: 2, username: 'jon' - }] - }; - // RocketChat.models.Rooms.findOneById(message.rid);, - }, - getChannels(channels) { - return [{ - _id: 1, - name: 'general' - }].filter(channel => channels.includes(channel.name)); - // return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); - } + }].filter(user => usernames.includes(user.username));//Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }, + getChannel: () => { + return { + usernames: [{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }] + }; + // RocketChat.models.Rooms.findOneById(message.rid);, + }, + getChannels(channels) { + return [{ + _id: 1, + name: 'general' + }].filter(channel => channels.includes(channel.name)); + // return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); + } + }); }); describe('Mention Server', () => { describe('getUsersByMentions', () => { describe('for @all but the number of users is greater than messageMaxAll', () => { - before(() => { + beforeEach(() => { mention.getChannel = () => { return { usernames:[{ @@ -73,7 +77,7 @@ describe('Mention Server', () => { }); }); describe('for one user', () => { - before(() => { + beforeEach(() => { mention.getChannel = () => { return { usernames:[{ -- GitLab From 2c8e466579f64813a6973a306aa7913e693aaf7c Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 10 May 2017 15:31:34 -0300 Subject: [PATCH 067/280] finish converting --- .../autocomplete-client.coffee | 367 ----------------- .../autocomplete-server.coffee | 27 -- packages/meteor-autocomplete/autocomplete.css | 27 -- .../client/autocomplete-client.coffee | 369 ------------------ .../client/autocomplete-client.js | 6 - .../client/templates.coffee | 50 --- .../meteor-autocomplete/client/templates.js | 80 ++++ packages/meteor-autocomplete/inputs.html | 39 -- packages/meteor-autocomplete/package.js | 7 +- .../server/autocomplete-server.coffee | 27 -- .../server/autocomplete-server.js | 31 ++ packages/meteor-autocomplete/templates.coffee | 50 --- 12 files changed, 114 insertions(+), 966 deletions(-) delete mode 100755 packages/meteor-autocomplete/autocomplete-client.coffee delete mode 100755 packages/meteor-autocomplete/autocomplete-server.coffee delete mode 100755 packages/meteor-autocomplete/autocomplete.css delete mode 100755 packages/meteor-autocomplete/client/autocomplete-client.coffee delete mode 100755 packages/meteor-autocomplete/client/templates.coffee create mode 100755 packages/meteor-autocomplete/client/templates.js delete mode 100755 packages/meteor-autocomplete/inputs.html delete mode 100755 packages/meteor-autocomplete/server/autocomplete-server.coffee create mode 100755 packages/meteor-autocomplete/server/autocomplete-server.js delete mode 100755 packages/meteor-autocomplete/templates.coffee diff --git a/packages/meteor-autocomplete/autocomplete-client.coffee b/packages/meteor-autocomplete/autocomplete-client.coffee deleted file mode 100755 index b588757851..0000000000 --- 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 192d5479bb..0000000000 --- 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/autocomplete.css b/packages/meteor-autocomplete/autocomplete.css deleted file mode 100755 index c4af66c2ff..0000000000 --- a/packages/meteor-autocomplete/autocomplete.css +++ /dev/null @@ -1,27 +0,0 @@ -.-autocomplete-container { - position: absolute; - background: white; - border: 1px solid #DDD; - border-radius: 3px; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); - min-width: 180px; - z-index: 1000; -} - -.-autocomplete-list { - list-style: none; - margin: 0; - padding: 0; -} - -.-autocomplete-item { - display: block; - padding: 5px 10px; - border-bottom: 1px solid #DDD; -} - -.-autocomplete-item.selected { - color: white; - background: #4183C4; - text-decoration: none; -} diff --git a/packages/meteor-autocomplete/client/autocomplete-client.coffee b/packages/meteor-autocomplete/client/autocomplete-client.coffee deleted file mode 100755 index 1996c1df53..0000000000 --- a/packages/meteor-autocomplete/client/autocomplete-client.coffee +++ /dev/null @@ -1,369 +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(".") - console.log(obj) - 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 - console.log(@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/client/autocomplete-client.js b/packages/meteor-autocomplete/client/autocomplete-client.js index 7f11465e1e..5adef251e2 100755 --- a/packages/meteor-autocomplete/client/autocomplete-client.js +++ b/packages/meteor-autocomplete/client/autocomplete-client.js @@ -452,9 +452,3 @@ this.AutoComplete = class { } }; - -// AutocompleteTest = { -// records: AutoCompleteRecords, -// getRegExp, -// getFindParams -// }; diff --git a/packages/meteor-autocomplete/client/templates.coffee b/packages/meteor-autocomplete/client/templates.coffee deleted file mode 100755 index cd56d8ba73..0000000000 --- a/packages/meteor-autocomplete/client/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/meteor-autocomplete/client/templates.js b/packages/meteor-autocomplete/client/templates.js new file mode 100755 index 0000000000..744b70097d --- /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) { + return t.ac.onKeyDown(e); + }, + 'keyup'(e, t) { + return t.ac.onKeyUp(e); + }, + 'focus'(e, t) { + return t.ac.onFocus(e); + }, + 'blur'(e, t) { + return 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(); + return 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() { + return this.data.tmplInst = this; +}; + +Template._autocompleteContainer.destroyed = function() { + // Meteor._debug "autocomplete destroyed" + return 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) { + return t.data.onItemClick(this, e); + }, + 'mouseenter .-autocomplete-item'(e, t) { + return 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/inputs.html b/packages/meteor-autocomplete/inputs.html deleted file mode 100755 index 289caa58ff..0000000000 --- a/packages/meteor-autocomplete/inputs.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js index e5da193ba0..ab887e0432 100755 --- a/packages/meteor-autocomplete/package.js +++ b/packages/meteor-autocomplete/package.js @@ -7,7 +7,7 @@ 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'); @@ -17,11 +17,11 @@ Package.onUse(function(api) { 'client/autocomplete.css', 'client/inputs.html', 'client/autocomplete-client.js', - 'client/templates.coffee' + 'client/templates.js' ], 'client'); api.addFiles([ - 'server/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.coffee b/packages/meteor-autocomplete/server/autocomplete-server.coffee deleted file mode 100755 index 192d5479bb..0000000000 --- a/packages/meteor-autocomplete/server/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/server/autocomplete-server.js b/packages/meteor-autocomplete/server/autocomplete-server.js new file mode 100755 index 0000000000..582daf40c1 --- /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 cd56d8ba73..0000000000 --- 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 -- GitLab From 2540f1525334bd1b47dc6d78651b04c1a7797e69 Mon Sep 17 00:00:00 2001 From: Aaron Ogle Date: Wed, 10 May 2017 13:38:46 -0500 Subject: [PATCH 068/280] dont try to show file preview on upload if unknown file type closes 6922 closes 6917 --- packages/rocketchat-ui/client/lib/fileUpload.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rocketchat-ui/client/lib/fileUpload.coffee b/packages/rocketchat-ui/client/lib/fileUpload.coffee index 55507fe061..6d5600628f 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 file.file.type is undefined + 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] -- GitLab From 000f8e8a2db6519f401562ab7a27799d1c8073cd Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 10 May 2017 14:50:56 -0300 Subject: [PATCH 069/280] Test ci --- .docker/{latest => }/Dockerfile | 2 +- .docker/develop/Dockerfile | 38 ---------------------- .docker/release-candidate/Dockerfile | 37 --------------------- .scripts/set-version.js | 48 ++++++++++++++++++++++++++++ .scripts/version.js | 36 ++------------------- .travis.yml | 1 - .travis/setartname.sh | 7 +--- package.json | 3 +- 8 files changed, 54 insertions(+), 118 deletions(-) rename .docker/{latest => }/Dockerfile (96%) delete mode 100644 .docker/develop/Dockerfile delete mode 100644 .docker/release-candidate/Dockerfile create mode 100644 .scripts/set-version.js diff --git a/.docker/latest/Dockerfile b/.docker/Dockerfile similarity index 96% rename from .docker/latest/Dockerfile rename to .docker/Dockerfile index 77b309ea58..cdda15514a 100644 --- a/.docker/latest/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM rocketchat/base:4 -ENV RC_VERSION latest +ENV RC_VERSION 0.56.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.docker/develop/Dockerfile b/.docker/develop/Dockerfile deleted file mode 100644 index b14fd89de4..0000000000 --- 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 192b8e95c0..0000000000 --- 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/.scripts/set-version.js b/.scripts/set-version.js new file mode 100644 index 0000000000..d5534f7a3f --- /dev/null +++ b/.scripts/set-version.js @@ -0,0 +1,48 @@ +/* eslint object-shorthand: 0, prefer-template: 0 */ + +const path = require('path'); +const fs = require('fs'); +let pkgJson = {}; + +try { + pkgJson = require(path.resolve( + process.cwd(), + './package.json' + )); +} catch (err) { + 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', + './.docker/Dockerfile.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'); + }); +}); diff --git a/.scripts/version.js b/.scripts/version.js index a99cf27994..c4aeb68acd 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 09bfc03484..3192c2d4b1 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 diff --git a/.travis/setartname.sh b/.travis/setartname.sh index cf8f9a150c..349280f820 100755 --- a/.travis/setartname.sh +++ b/.travis/setartname.sh @@ -1,6 +1 @@ -if [[ $TRAVIS_TAG ]] - then - export ARTIFACT_NAME="$TRAVIS_TAG"; -else - export ARTIFACT_NAME="$TRAVIS_BRANCH"; -fi +export ARTIFACT_NAME="$(meteor npm run version --silent)" diff --git a/package.json b/package.json index dc56f1e493..2302bf86fb 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "chimp-test": "chimp tests/chimp-config.js", "postinstall": "cd packages/rocketchat-katex && npm i", "version": "node .scripts/version.js", - "release": "npm run version && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" + "get-version": "node .scripts/get-version.js", + "release": "npm run get-version && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" }, "license": "MIT", "repository": { -- GitLab From e9e386cebfab18853f4be0b6bd046837d7d00310 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 10 May 2017 17:49:59 -0300 Subject: [PATCH 070/280] Fix file name --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2302bf86fb..bdd79038b7 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "chimp-test": "chimp tests/chimp-config.js", "postinstall": "cd packages/rocketchat-katex && npm i", "version": "node .scripts/version.js", - "get-version": "node .scripts/get-version.js", - "release": "npm run get-version && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" + "set-version": "node .scripts/set-version.js", + "release": "npm run set-version && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" }, "license": "MIT", "repository": { -- GitLab From 11d1871750f6deb4bac332578b4f8dd337014c7a Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Wed, 10 May 2017 22:28:35 -0300 Subject: [PATCH 071/280] Fix saving/resetting roomPick settings --- .../.npm/package/npm-shrinkwrap.json | 195 ++++++++++++++---- .../server/methods/saveSetting.js | 2 +- .../rocketchat-ui-admin/client/admin.coffee | 8 +- 3 files changed, 166 insertions(+), 39 deletions(-) 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 c931564c62..758f653401 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -30,6 +30,11 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "from": "arrify@>=1.0.1 <2.0.0" }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "from": "ascli@>=1.0.0 <2.0.0" + }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -52,7 +57,7 @@ }, "aws-sign2": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "resolved": "http://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", "from": "aws-sign2@>=0.6.0 <0.7.0" }, "aws4": { @@ -60,6 +65,11 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "from": "aws4@>=1.2.1 <2.0.0" }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "from": "balanced-match@>=0.4.1 <0.5.0" + }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", @@ -75,6 +85,11 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "from": "boom@>=2.0.0 <3.0.0" }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "from": "brace-expansion@>=1.1.7 <2.0.0" + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -85,6 +100,16 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "from": "buffer-shims@>=1.0.0 <1.1.0" }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "from": "bytebuffer@>=5.0.0 <6.0.0" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "from": "camelcase@>=2.0.1 <3.0.0" + }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -100,11 +125,26 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "from": "chalk@>=1.1.1 <2.0.0" }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "from": "cliui@>=3.0.3 <4.0.0" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "from": "co@>=4.6.0 <5.0.0" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "from": "code-point-at@>=1.0.0 <2.0.0" + }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "from": "colour@>=0.7.1 <0.8.0" + }, "combined-stream": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -115,6 +155,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "from": "commander@>=2.9.0 <3.0.0" }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "from": "concat-map@0.0.1" + }, "concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", @@ -122,7 +167,7 @@ }, "core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "from": "core-util-is@>=1.0.0 <1.1.0" }, "create-error-class": { @@ -147,6 +192,11 @@ } } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "from": "decamelize@>=1.1.1 <2.0.0" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -202,6 +252,11 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", "from": "form-data@>=2.1.1 <2.2.0" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "from": "fs.realpath@>=1.0.0 <2.0.0" + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -224,6 +279,11 @@ } } }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "from": "glob@>=7.0.5 <8.0.0" + }, "google-auth-library": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz", @@ -262,8 +322,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.0.tgz", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.1.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -511,8 +571,8 @@ } }, "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "from": "extend@>=3.0.0 <3.1.0" }, "forever-agent": { @@ -538,8 +598,8 @@ "from": "har-validator@>=4.2.1 <4.3.0", "dependencies": { "ajv": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "from": "ajv@>=4.9.1 <5.0.0", "dependencies": { "co": { @@ -879,8 +939,8 @@ "from": "tar-pack@>=3.4.0 <4.0.0", "dependencies": { "debug": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.5.tgz", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz", "from": "debug@>=2.2.0 <3.0.0", "dependencies": { "ms": { @@ -903,7 +963,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" } } }, @@ -946,7 +1006,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.3.3 <2.0.0", + "from": "once@>=1.0.0 <2.0.0", "dependencies": { "wrappy": { "version": "1.0.2", @@ -973,7 +1033,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" }, "isarray": { "version": "1.0.0", @@ -1043,16 +1103,31 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "from": "http-signature@>=1.1.0 <1.2.0" }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "from": "inflight@>=1.0.4 <2.0.0" + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "from": "inherits@>=2.0.3 <3.0.0" }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "from": "invert-kv@>=1.0.0 <2.0.0" + }, "is": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", "from": "is@>=3.0.1 <4.0.0" }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0" + }, "is-my-json-valid": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", @@ -1064,8 +1139,8 @@ "from": "is-property@>=1.0.0 <2.0.0" }, "is-stream-ended": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.0.tgz", + "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": { @@ -1140,6 +1215,11 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", "from": "jws@>=3.1.4 <4.0.0" }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "from": "lcid@>=1.0.0 <2.0.0" + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -1158,7 +1238,7 @@ "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "from": "long@>=3.2.0 <4.0.0" + "from": "long@>=3.0.0 <4.0.0" }, "methmeth": { "version": "1.1.0", @@ -1180,6 +1260,11 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "from": "mime-types@>=2.1.7 <2.2.0" }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "from": "minimatch@>=3.0.2 <4.0.0" + }, "modelo": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz", @@ -1195,6 +1280,11 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "from": "node-forge@>=0.7.1 <0.8.0" }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "from": "number-is-nan@>=1.0.0 <2.0.0" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -1210,6 +1300,21 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "from": "once@>=1.3.0 <1.4.0" }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "from": "optjs@>=3.2.2 <3.3.0" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "from": "os-locale@>=1.4.0 <2.0.0" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "from": "path-is-absolute@>=1.0.0 <2.0.0" + }, "performance-now": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", @@ -1236,9 +1341,9 @@ "from": "propprop@>=0.3.1 <0.4.0" }, "protobufjs": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.7.3.tgz", - "from": "protobufjs@>=6.7.0 <7.0.0" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", + "from": "protobufjs@>=5.0.0 <6.0.0" }, "punycode": { "version": "1.4.1", @@ -1267,7 +1372,7 @@ "dependencies": { "caseless": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "resolved": "http://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", "from": "caseless@>=0.11.0 <0.12.0" }, "har-validator": { @@ -1308,16 +1413,9 @@ "from": "sntp@>=1.0.0 <2.0.0" }, "split-array-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.0.tgz", - "from": "split-array-stream@>=1.0.0 <2.0.0", - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "from": "async@>=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", @@ -1332,8 +1430,8 @@ } }, "stream-events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.1.tgz", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", "from": "stream-events@>=1.0.1 <2.0.0" }, "stream-shift": { @@ -1346,6 +1444,11 @@ "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz", "from": "string-format-obj@>=1.1.0 <2.0.0" }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "from": "string-width@>=1.0.1 <2.0.0" + }, "string_decoder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz", @@ -1362,9 +1465,9 @@ "from": "strip-ansi@>=3.0.0 <4.0.0" }, "stubs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-1.1.2.tgz", - "from": "stubs@>=1.1.0 <2.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "from": "stubs@>=3.0.0 <4.0.0" }, "supports-color": { "version": "2.0.0", @@ -1398,7 +1501,7 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "from": "util-deprecate@>=1.0.1 <1.1.0" }, "uuid": { @@ -1411,6 +1514,16 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "from": "verror@1.3.6" }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "from": "window-size@>=0.1.4 <0.2.0" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "from": "wrap-ansi@>=2.0.0 <3.0.0" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1420,6 +1533,16 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "from": "xtend@>=4.0.0 <5.0.0" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "from": "y18n@>=3.2.0 <4.0.0" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "from": "yargs@>=3.10.0 <4.0.0" } } } diff --git a/packages/rocketchat-lib/server/methods/saveSetting.js b/packages/rocketchat-lib/server/methods/saveSetting.js index 2c0402317b..ddee100545 100644 --- a/packages/rocketchat-lib/server/methods/saveSetting.js +++ b/packages/rocketchat-lib/server/methods/saveSetting.js @@ -20,7 +20,7 @@ Meteor.methods({ //Verify the value is what it should be switch (setting.type) { case 'roomPick': - check(value, [Object]); + check(value, Match.OneOf([Object], '')); break; case 'boolean': check(value, Boolean); diff --git a/packages/rocketchat-ui-admin/client/admin.coffee b/packages/rocketchat-ui-admin/client/admin.coffee index 5f86c8d750..9ca4269d86 100644 --- a/packages/rocketchat-ui-admin/client/admin.coffee +++ b/packages/rocketchat-ui-admin/client/admin.coffee @@ -23,6 +23,10 @@ setFieldValue = (settingId, value, type, editor) -> selectedRooms = Template.instance().selectedRooms.get() selectedRooms[settingId] = value Template.instance().selectedRooms.set(selectedRooms) + TempSettings.update {_id: settingId}, + $set: + value: value + changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(settingId).value) isnt JSON.stringify(value) else input.val(value).change() @@ -460,7 +464,7 @@ Template.admin.events TempSettings.update {_id: this.id}, $set: value: value - changed: RocketChat.settings.collectionPrivate.findOne(this.id).value isnt value + changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(this.id).value) isnt JSON.stringify(value) event.currentTarget.value = '' event.currentTarget.focus() @@ -474,7 +478,7 @@ Template.admin.events TempSettings.update {_id: settingId}, $set: value: value - changed: RocketChat.settings.collectionPrivate.findOne(settingId).value isnt value + changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(settingId).value) isnt JSON.stringify(value) Template.admin.onRendered -> Tracker.afterFlush -> -- GitLab From f8c8633b38ac79695c396ab17735ef69ed2d43a4 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 10 May 2017 23:20:26 -0300 Subject: [PATCH 072/280] Fix versions and add build number to develop build --- .docker/Dockerfile | 2 +- .sandstorm/sandstorm-pkgdef.capnp | 2 +- .travis/setartname.sh | 7 ++++++- .travis/snap.sh | 2 +- package.json | 2 +- packages/rocketchat-lib/rocketchat.info | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.docker/Dockerfile b/.docker/Dockerfile index cdda15514a..beaccef615 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM rocketchat/base:4 -ENV RC_VERSION 0.56.0-develop +ENV RC_VERSION 0.57.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp index 14ee2003b1..c794307a7d 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/.travis/setartname.sh b/.travis/setartname.sh index 349280f820..35ba805688 100755 --- a/.travis/setartname.sh +++ b/.travis/setartname.sh @@ -1 +1,6 @@ -export ARTIFACT_NAME="$(meteor npm run version --silent)" +if [[ $TRAVIS_BRANCH ]] + then + export ARTIFACT_NAME="$(meteor npm run version --silent).$TRAVIS_BUILD_NUMBER" +else + export ARTIFACT_NAME="$(meteor npm run version --silent)" +fi diff --git a/.travis/snap.sh b/.travis/snap.sh index d5ad25ba5c..8b602fa558 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/package.json b/package.json index bdd79038b7..ec5c5b29e0 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/" diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info index ce43333a4e..fbad42938b 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" } -- GitLab From fe6bc9626d37f962241aec6148cd3779b9d9f7bf Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 11 May 2017 09:53:43 -0300 Subject: [PATCH 073/280] Use AWS Signature Version 4 signed URLs for uploads Closes #3099 --- .../server/config/configFileUploadAmazonS3.js | 24 ++- .../rocketchat-file-upload/server/lib/AWS4.js | 175 ++++++++++++++++++ .../server/methods/getS3FileUrl.js | 25 ++- 3 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 packages/rocketchat-file-upload/server/lib/AWS4.js diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index daac0ea199..e217c963e3 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 0000000000..bb8980ac0d --- /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 ef48ddc216..c3cbca844e 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 }`; } }); -- GitLab From 790310842e786c705cd5f42d1d8e2ccaed143df6 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 11 May 2017 12:19:45 -0300 Subject: [PATCH 074/280] Fix badge counter on iOS push notifications Closes #6356 --- .../server/lib/PushNotification.js | 4 +- .../server/lib/sendNotificationsOnMessage.js | 43 ++++++++++++------- .../server/models/Subscriptions.coffee | 8 ++++ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/rocketchat-lib/server/lib/PushNotification.js b/packages/rocketchat-lib/server/lib/PushNotification.js index fd1bdb8db6..fca87e6455 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 ea6ef47011..b4802e357f 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 95f12bff51..2eb4f13d48 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 = -- GitLab From ea7a21f56b121953f33b2af2fc6d5cfffd0b9262 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 11 May 2017 12:48:34 -0300 Subject: [PATCH 075/280] fix reviews --- .../client/accountPreferences.js | 22 ++-- .../client/accountProfile.js | 108 ++++++++---------- .../client/avatar/avatar.js | 2 +- .../client/avatar/prompt.js | 14 +-- 4 files changed, 68 insertions(+), 78 deletions(-) diff --git a/packages/rocketchat-ui-account/client/accountPreferences.js b/packages/rocketchat-ui-account/client/accountPreferences.js index 6e996392b4..4ae3b54be5 100644 --- a/packages/rocketchat-ui-account/client/accountPreferences.js +++ b/packages/rocketchat-ui-account/client/accountPreferences.js @@ -14,13 +14,11 @@ Template.accountPreferences.helpers({ }, languages() { const languages = TAPi18n.getLanguages(); - const result = []; - Object.keys(languages).forEach((key) => { + + const result = Object.keys(languages).map((key) => { const language = languages[key]; - result.push(_.extend(language, { - key - })); + return _.extend(language, { key }); }); return _.sortBy(result, 'key'); @@ -37,20 +35,22 @@ Template.accountPreferences.helpers({ }, checked(property, value, defaultValue) { const user = Meteor.user(); + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); let currentValue; - if (user && user.settings && user.settings.preferences && user.settings.preferences[property] && defaultValue === true) { - currentValue = value; - } else if (user && user.settings && user.settings.preferences && user.settings.preferences[property]) { + 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(); - if (!(user && user.settings && user.settings.preferences && user.settings.preferences[property])) { - return defaultValue === true; + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); + if (propertyeExists) { + return user.settings.preferences[property] === value; } else { - return (user && user.settings && user.settings.preferences && user.settings.preferences[property]) === value; + return defaultValue === true; } }, highlights() { diff --git a/packages/rocketchat-ui-account/client/accountProfile.js b/packages/rocketchat-ui-account/client/accountProfile.js index 0db7192d3e..2c50ea42b5 100644 --- a/packages/rocketchat-ui-account/client/accountProfile.js +++ b/packages/rocketchat-ui-account/client/accountProfile.js @@ -133,17 +133,15 @@ Template.accountProfile.events({ closeOnConfirm: false, confirmButtonText: t('Save'), cancelButtonText: t('Cancel') - }, () => { - return function(typedPassword) { - if (typedPassword) { - toastr.remove(); - toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); - return instance.save(SHA256(typedPassword)); - } else { - swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); - return false; - } - }; + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); + return 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'() { @@ -170,24 +168,22 @@ Template.accountProfile.events({ closeOnConfirm: false, confirmButtonText: t('Delete'), cancelButtonText: t('Cancel') - }, () => { - return function(typedPassword) { - if (typedPassword) { - toastr.remove(); - toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); - return Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { - if (error) { - toastr.remove(); - return swal.showInputError(t('Your_password_is_wrong')); - } else { - return swal.close(); - } - }); - } else { - swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); - return false; - } - }; + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + return Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { + if (error) { + toastr.remove(); + return swal.showInputError(t('Your_password_is_wrong')); + } else { + return swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } }); } else { return swal({ @@ -198,25 +194,23 @@ Template.accountProfile.events({ closeOnConfirm: false, confirmButtonText: t('Delete'), cancelButtonText: t('Cancel') - }, () => { - return function(deleteConfirmation) { - const user = Meteor.user(); - if (deleteConfirmation === (user && user.username)) { - toastr.remove(); - toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); - return Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { - if (error) { - toastr.remove(); - return swal.showInputError(t('Your_password_is_wrong')); - } else { - return swal.close(); - } - }); - } else { - swal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); - return false; - } - }; + }, (deleteConfirmation) => { + const user = Meteor.user(); + if (deleteConfirmation === (user && user.username)) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + return Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { + if (error) { + toastr.remove(); + return swal.showInputError(t('Your_password_is_wrong')); + } else { + return swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); + return false; + } }); } }, @@ -225,16 +219,14 @@ Template.accountProfile.events({ e.preventDefault(); e.currentTarget.innerHTML = `${ e.currentTarget.innerHTML } ...`; e.currentTarget.disabled = true; - return Meteor.call('sendConfirmationEmail', user.emails && user.emails[0] && user.emails[0].address(() => { - return function(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; - }; + return 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.js b/packages/rocketchat-ui-account/client/avatar/avatar.js index cfee03985a..74214467cc 100644 --- a/packages/rocketchat-ui-account/client/avatar/avatar.js +++ b/packages/rocketchat-ui-account/client/avatar/avatar.js @@ -1,7 +1,7 @@ Template.avatar.helpers({ imageUrl() { let username = this.username; - if ((username == null) && (this.userId != null)) { + if (username == null && this.userId != null) { const user = Meteor.users.findOne(this.userId); username = user && user.username; } diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.js b/packages/rocketchat-ui-account/client/avatar/prompt.js index 8c607bf736..c0ca457ae6 100644 --- a/packages/rocketchat-ui-account/client/avatar/prompt.js +++ b/packages/rocketchat-ui-account/client/avatar/prompt.js @@ -97,14 +97,12 @@ Template.avatarPrompt.events({ const loginWithService = `loginWith${ _.capitalize(this) }`; const serviceConfig = {}; return Meteor[loginWithService](serviceConfig, function(error) { - if ((error && error.error) === 'github-no-public-email') { - alert(t('github_no_public_email')); - return; - } - console.log(error); - if (error != null) { - toastr.error(error.message); - return; + 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); } return template.getSuggestions(); }); -- GitLab From f21f0fb5507c19b99e82d7e24a240b4144327d26 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 11 May 2017 12:48:34 -0300 Subject: [PATCH 076/280] fix reviews --- .../client/accountPreferences.js | 26 ++--- .../client/accountProfile.js | 108 ++++++++---------- .../client/avatar/avatar.js | 2 +- .../client/avatar/prompt.js | 14 +-- 4 files changed, 70 insertions(+), 80 deletions(-) diff --git a/packages/rocketchat-ui-account/client/accountPreferences.js b/packages/rocketchat-ui-account/client/accountPreferences.js index 6e996392b4..4030a20750 100644 --- a/packages/rocketchat-ui-account/client/accountPreferences.js +++ b/packages/rocketchat-ui-account/client/accountPreferences.js @@ -14,13 +14,11 @@ Template.accountPreferences.helpers({ }, languages() { const languages = TAPi18n.getLanguages(); - const result = []; - Object.keys(languages).forEach((key) => { + + const result = Object.keys(languages).map((key) => { const language = languages[key]; - result.push(_.extend(language, { - key - })); + return _.extend(language, { key }); }); return _.sortBy(result, 'key'); @@ -37,20 +35,22 @@ Template.accountPreferences.helpers({ }, checked(property, value, defaultValue) { const user = Meteor.user(); + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); let currentValue; - if (user && user.settings && user.settings.preferences && user.settings.preferences[property] && defaultValue === true) { - currentValue = value; - } else if (user && user.settings && user.settings.preferences && user.settings.preferences[property]) { + 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(); - if (!(user && user.settings && user.settings.preferences && user.settings.preferences[property])) { - return defaultValue === true; + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); + if (propertyeExists) { + return user.settings.preferences[property] === value; } else { - return (user && user.settings && user.settings.preferences && user.settings.preferences[property]) === value; + return defaultValue === true; } }, highlights() { @@ -58,10 +58,10 @@ Template.accountPreferences.helpers({ 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'); + return KonchatNotification.notificationStatus.get() === 'granted' || (window.Notification && Notification.permission === 'granted'); }, desktopNotificationDisabled() { - return (KonchatNotification.notificationStatus.get() === 'denied') || (window.Notification && Notification.permission === 'denied'); + return KonchatNotification.notificationStatus.get() === 'denied' || (window.Notification && Notification.permission === 'denied'); }, desktopNotificationDuration() { const user = Meteor.user(); diff --git a/packages/rocketchat-ui-account/client/accountProfile.js b/packages/rocketchat-ui-account/client/accountProfile.js index 0db7192d3e..2c50ea42b5 100644 --- a/packages/rocketchat-ui-account/client/accountProfile.js +++ b/packages/rocketchat-ui-account/client/accountProfile.js @@ -133,17 +133,15 @@ Template.accountProfile.events({ closeOnConfirm: false, confirmButtonText: t('Save'), cancelButtonText: t('Cancel') - }, () => { - return function(typedPassword) { - if (typedPassword) { - toastr.remove(); - toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); - return instance.save(SHA256(typedPassword)); - } else { - swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); - return false; - } - }; + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); + return 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'() { @@ -170,24 +168,22 @@ Template.accountProfile.events({ closeOnConfirm: false, confirmButtonText: t('Delete'), cancelButtonText: t('Cancel') - }, () => { - return function(typedPassword) { - if (typedPassword) { - toastr.remove(); - toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); - return Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { - if (error) { - toastr.remove(); - return swal.showInputError(t('Your_password_is_wrong')); - } else { - return swal.close(); - } - }); - } else { - swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); - return false; - } - }; + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + return Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { + if (error) { + toastr.remove(); + return swal.showInputError(t('Your_password_is_wrong')); + } else { + return swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } }); } else { return swal({ @@ -198,25 +194,23 @@ Template.accountProfile.events({ closeOnConfirm: false, confirmButtonText: t('Delete'), cancelButtonText: t('Cancel') - }, () => { - return function(deleteConfirmation) { - const user = Meteor.user(); - if (deleteConfirmation === (user && user.username)) { - toastr.remove(); - toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); - return Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { - if (error) { - toastr.remove(); - return swal.showInputError(t('Your_password_is_wrong')); - } else { - return swal.close(); - } - }); - } else { - swal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); - return false; - } - }; + }, (deleteConfirmation) => { + const user = Meteor.user(); + if (deleteConfirmation === (user && user.username)) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + return Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { + if (error) { + toastr.remove(); + return swal.showInputError(t('Your_password_is_wrong')); + } else { + return swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); + return false; + } }); } }, @@ -225,16 +219,14 @@ Template.accountProfile.events({ e.preventDefault(); e.currentTarget.innerHTML = `${ e.currentTarget.innerHTML } ...`; e.currentTarget.disabled = true; - return Meteor.call('sendConfirmationEmail', user.emails && user.emails[0] && user.emails[0].address(() => { - return function(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; - }; + return 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.js b/packages/rocketchat-ui-account/client/avatar/avatar.js index cfee03985a..74214467cc 100644 --- a/packages/rocketchat-ui-account/client/avatar/avatar.js +++ b/packages/rocketchat-ui-account/client/avatar/avatar.js @@ -1,7 +1,7 @@ Template.avatar.helpers({ imageUrl() { let username = this.username; - if ((username == null) && (this.userId != null)) { + if (username == null && this.userId != null) { const user = Meteor.users.findOne(this.userId); username = user && user.username; } diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.js b/packages/rocketchat-ui-account/client/avatar/prompt.js index 8c607bf736..c0ca457ae6 100644 --- a/packages/rocketchat-ui-account/client/avatar/prompt.js +++ b/packages/rocketchat-ui-account/client/avatar/prompt.js @@ -97,14 +97,12 @@ Template.avatarPrompt.events({ const loginWithService = `loginWith${ _.capitalize(this) }`; const serviceConfig = {}; return Meteor[loginWithService](serviceConfig, function(error) { - if ((error && error.error) === 'github-no-public-email') { - alert(t('github_no_public_email')); - return; - } - console.log(error); - if (error != null) { - toastr.error(error.message); - return; + 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); } return template.getSuggestions(); }); -- GitLab From 559e535ebc160a64e6651812a828ab996c07c91b Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 11 May 2017 13:49:41 -0300 Subject: [PATCH 077/280] fix conflicts --- packages/rocketchat-webrtc/WebRTCClass.coffee | 823 ------------------ .../rocketchat-webrtc/client/WebRTCClass.js | 13 +- 2 files changed, 9 insertions(+), 827 deletions(-) delete mode 100644 packages/rocketchat-webrtc/WebRTCClass.coffee diff --git a/packages/rocketchat-webrtc/WebRTCClass.coffee b/packages/rocketchat-webrtc/WebRTCClass.coffee deleted file mode 100644 index a957e411d7..0000000000 --- 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