diff --git a/packages/rocketchat-lib/client/MessageAction.coffee b/packages/rocketchat-lib/client/MessageAction.coffee deleted file mode 100644 index 80837c3a0beb..000000000000 --- a/packages/rocketchat-lib/client/MessageAction.coffee +++ /dev/null @@ -1,247 +0,0 @@ -import moment from 'moment' -import toastr from 'toastr' - -RocketChat.MessageAction = new class - buttons = new ReactiveVar {} - - ### - config expects the following keys (only id is mandatory): - id (mandatory) - icon: string - i18nLabel: string - action: function(event, instance) - validation: function(message) - order: integer - ### - addButton = (config) -> - unless config?.id - return false - - Tracker.nonreactive -> - btns = buttons.get() - btns[config.id] = config - buttons.set btns - - removeButton = (id) -> - Tracker.nonreactive -> - btns = buttons.get() - delete btns[id] - buttons.set btns - - updateButton = (id, config) -> - Tracker.nonreactive -> - btns = buttons.get() - if btns[id] - btns[id] = _.extend btns[id], config - buttons.set btns - - getButtonById = (id) -> - allButtons = buttons.get() - return allButtons[id] - - getButtons = (message, context) -> - allButtons = _.toArray buttons.get() - if message - allowedButtons = _.compact _.map allButtons, (button) -> - if not button.context? or button.context.indexOf(context) > -1 - if not button.validation? or button.validation(message, context) - return button - else - allowedButtons = allButtons - - return _.sortBy allowedButtons, 'order' - - resetButtons = -> - buttons.set {} - - getPermaLink = (msgId) -> - roomData = ChatSubscription.findOne({rid: Session.get('openedRoom')}) - if roomData - routePath = RocketChat.roomTypes.getRouteLink(roomData.t, roomData) - else - routePath = document.location.pathname - return Meteor.absoluteUrl().replace(/\/$/, '') + routePath + '?msg=' + msgId - - hideDropDown = () -> - $('.message-dropdown:visible').hide() - - addButton: addButton - removeButton: removeButton - updateButton: updateButton - getButtons: getButtons - getButtonById: getButtonById - resetButtons: resetButtons - getPermaLink: getPermaLink - hideDropDown: hideDropDown - -Meteor.startup -> - - $(document).click (event) => - target = $(event.target) - if !target.closest('.message-cog-container').length and !target.is('.message-cog-container') - RocketChat.MessageAction.hideDropDown() - - RocketChat.MessageAction.addButton - id: 'reply-message' - icon: 'icon-reply' - i18nLabel: 'Reply' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - input = instance.find('.input-message') - url = RocketChat.MessageAction.getPermaLink(message._id) - text = '[ ](' + url + ') @' + message.u.username + ' ' - if input.value - input.value += if input.value.endsWith(' ') then '' else ' ' - input.value += text - input.focus() - RocketChat.MessageAction.hideDropDown() - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - return true - order: 1 - - RocketChat.MessageAction.addButton - id: 'edit-message' - icon: 'icon-pencil' - i18nLabel: 'Edit' - context: [ - 'message' - 'message-mobile' - ] - action: (e, instance) -> - message = $(e.currentTarget).closest('.message')[0] - chatMessages[Session.get('openedRoom')].edit(message) - RocketChat.MessageAction.hideDropDown() - input = instance.find('.input-message') - Meteor.setTimeout -> - input.focus() - input.updateAutogrow() - , 200 - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid) - isEditAllowed = RocketChat.settings.get 'Message_AllowEditing' - editOwn = message.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(message.ts) if message.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockEditInMinutes - else - return true - order: 2 - - RocketChat.MessageAction.addButton - id: 'delete-message' - icon: 'icon-trash-alt' - i18nLabel: 'Delete' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - RocketChat.MessageAction.hideDropDown() - chatMessages[Session.get('openedRoom')].confirmDeleteMsg(message) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid) - isDeleteAllowed = RocketChat.settings.get 'Message_AllowDeleting' - deleteOwn = message.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(message.ts) if message.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockDeleteInMinutes - else - return true - order: 3 - - RocketChat.MessageAction.addButton - id: 'permalink' - icon: 'icon-link' - i18nLabel: 'Permalink' - classes: 'clipboard' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - permalink = RocketChat.MessageAction.getPermaLink(message._id) - RocketChat.MessageAction.hideDropDown() - if Meteor.isCordova - cordova.plugins.clipboard.copy(permalink); - else - $(event.currentTarget).attr('data-clipboard-text', permalink); - toastr.success(TAPi18n.__('Copied')) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 4 - - RocketChat.MessageAction.addButton - id: 'copy' - icon: 'icon-paste' - i18nLabel: 'Copy' - classes: 'clipboard' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1].msg - RocketChat.MessageAction.hideDropDown() - if Meteor.isCordova - cordova.plugins.clipboard.copy(message); - else - $(event.currentTarget).attr('data-clipboard-text', message) - toastr.success(TAPi18n.__('Copied')) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 5 - - RocketChat.MessageAction.addButton - id: 'quote-message' - icon: 'icon-quote-left' - i18nLabel: 'Quote' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - input = instance.find('.input-message') - url = RocketChat.MessageAction.getPermaLink(message._id) - text = '[ ](' + url + ') ' - if input.value - input.value += if input.value.endsWith(' ') then '' else ' ' - input.value += text - input.focus() - RocketChat.MessageAction.hideDropDown() - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 6 diff --git a/packages/rocketchat-lib/client/MessageAction.js b/packages/rocketchat-lib/client/MessageAction.js new file mode 100644 index 000000000000..db2ee342a494 --- /dev/null +++ b/packages/rocketchat-lib/client/MessageAction.js @@ -0,0 +1,287 @@ +import moment from 'moment'; + +import toastr from 'toastr'; + +RocketChat.MessageAction = new class { + /* + config expects the following keys (only id is mandatory): + id (mandatory) + icon: string + i18nLabel: string + action: function(event, instance) + validation: function(message) + order: integer + */ + + constructor() { + this.buttons = new ReactiveVar({}); + } + + addButton(config) { + if (!config || !config.id) { + return false; + } + return Tracker.nonreactive(() => { + const btns = this.buttons.get(); + btns[config.id] = config; + return this.buttons.set(btns); + }); + } + + removeButton(id) { + return Tracker.nonreactive(() => { + const btns = this.buttons.get(); + delete btns[id]; + return this.buttons.set(btns); + }); + } + + updateButton(id, config) { + return Tracker.nonreactive(() => { + const btns = this.buttons.get(); + if (btns[id]) { + btns[id] = _.extend(btns[id], config); + return this.buttons.set(btns); + } + }); + } + + getButtonById(id) { + const allButtons = this.buttons.get(); + return allButtons[id]; + } + + getButtons(message, context) { + const allButtons = _.toArray(this.buttons.get()); + let allowedButtons = allButtons; + if (message) { + allowedButtons = _.compact(_.map(allButtons, function(button) { + if (button.context == null || button.context.includes(context)) { + if (button.validation == null || button.validation(message, context)) { + return button; + } + } + })); + } + return _.sortBy(allowedButtons, 'order'); + } + + resetButtons() { + return this.buttons.set({}); + } + + getPermaLink(msgId) { + const roomData = ChatSubscription.findOne({ + rid: Session.get('openedRoom') + }); + let routePath = document.location.pathname; + if (roomData) { + routePath = RocketChat.roomTypes.getRouteLink(roomData.t, roomData); + } + return `${ Meteor.absoluteUrl().replace(/\/$/, '') + routePath }?msg=${ msgId }`; + } + + hideDropDown() { + return $('.message-dropdown:visible').hide(); + } +}; + +Meteor.startup(function() { + $(document).click((event) => { + const target = $(event.target); + if (!target.closest('.message-cog-container').length && !target.is('.message-cog-container')) { + return RocketChat.MessageAction.hideDropDown(); + } + }); + + RocketChat.MessageAction.addButton({ + id: 'reply-message', + icon: 'icon-reply', + i18nLabel: 'Reply', + context: ['message', 'message-mobile'], + action(event, instance) { + const message = this._arguments[1]; + const input = instance.find('.input-message'); + const url = RocketChat.MessageAction.getPermaLink(message._id); + const text = `[ ](${ url }) @${ message.u.username } `; + if (input.value) { + input.value += input.value.endsWith(' ') ? '' : ' '; + } + input.value += text; + input.focus(); + return RocketChat.MessageAction.hideDropDown(); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 1 + }); + /* globals chatMessages*/ + RocketChat.MessageAction.addButton({ + id: 'edit-message', + icon: 'icon-pencil', + i18nLabel: 'Edit', + context: ['message', 'message-mobile'], + action(e, instance) { + const message = $(e.currentTarget).closest('.message')[0]; + chatMessages[Session.get('openedRoom')].edit(message); + RocketChat.MessageAction.hideDropDown(); + const input = instance.find('.input-message'); + Meteor.setTimeout(() => { + input.focus(); + input.updateAutogrow(); + }, 200); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid); + const isEditAllowed = RocketChat.settings.get('Message_AllowEditing'); + const editOwn = message.u && message.u._id === Meteor.userId(); + if (!(hasPermission || (isEditAllowed && editOwn))) { + return; + } + const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); + if (blockEditInMinutes) { + let msgTs; + if (message.ts != null) { + msgTs = moment(message.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockEditInMinutes; + } else { + return true; + } + }, + order: 2 + }); + RocketChat.MessageAction.addButton({ + id: 'delete-message', + icon: 'icon-trash-alt', + i18nLabel: 'Delete', + context: ['message', 'message-mobile'], + action() { + const message = this._arguments[1]; + RocketChat.MessageAction.hideDropDown(); + return chatMessages[Session.get('openedRoom')].confirmDeleteMsg(message); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({rid: message.rid}) == null) { + return false; + } + 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))) { + return; + } + 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'); + } + return currentTsDiff < blockDeleteInMinutes; + } else { + return true; + } + }, + order: 3 + }); + /* globals cordova*/ + RocketChat.MessageAction.addButton({ + id: 'permalink', + icon: 'icon-link', + i18nLabel: 'Permalink', + classes: 'clipboard', + context: ['message', 'message-mobile'], + action() { + const message = this._arguments[1]; + const permalink = RocketChat.MessageAction.getPermaLink(message._id); + RocketChat.MessageAction.hideDropDown(); + if (Meteor.isCordova) { + cordova.plugins.clipboard.copy(permalink); + } else { + $(event.currentTarget).attr('data-clipboard-text', permalink); + } + return toastr.success(TAPi18n.__('Copied')); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 4 + }); + RocketChat.MessageAction.addButton({ + id: 'copy', + icon: 'icon-paste', + i18nLabel: 'Copy', + classes: 'clipboard', + context: ['message', 'message-mobile'], + action() { + const message = this._arguments[1].msg; + RocketChat.MessageAction.hideDropDown(); + if (Meteor.isCordova) { + cordova.plugins.clipboard.copy(message); + } else { + $(event.currentTarget).attr('data-clipboard-text', message); + } + return toastr.success(TAPi18n.__('Copied')); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 5 + }); + return RocketChat.MessageAction.addButton({ + id: 'quote-message', + icon: 'icon-quote-left', + i18nLabel: 'Quote', + context: ['message', 'message-mobile'], + action(event, instance) { + const message = this._arguments[1]; + const input = instance.find('.input-message'); + const url = RocketChat.MessageAction.getPermaLink(message._id); + const text = `[ ](${ url }) `; + if (input.value) { + input.value += input.value.endsWith(' ') ? '' : ' '; + } + input.value += text; + input.focus(); + return RocketChat.MessageAction.hideDropDown(); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 6 + }); +}); diff --git a/packages/rocketchat-lib/client/Notifications.coffee b/packages/rocketchat-lib/client/Notifications.coffee deleted file mode 100644 index 9a51ceca3482..000000000000 --- a/packages/rocketchat-lib/client/Notifications.coffee +++ /dev/null @@ -1,72 +0,0 @@ -RocketChat.Notifications = new class - constructor: -> - @logged = Meteor.userId() isnt null - @loginCb = [] - Tracker.autorun => - if Meteor.userId() isnt null and this.logged is false - cb() for cb in this.loginCb - - @logged = Meteor.userId() isnt null - - @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' - - if @debug is true - @onAll -> console.log "RocketChat.Notifications: onAll", arguments - @onUser -> console.log "RocketChat.Notifications: onAll", arguments - - onLogin: (cb) -> - @loginCb.push(cb) - if @logged - cb() - - notifyRoom: (room, eventName, args...) -> - console.log "RocketChat.Notifications: notifyRoom", arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emit.apply @streamRoom, args - - notifyUser: (userId, eventName, args...) -> - console.log "RocketChat.Notifications: notifyUser", arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emit.apply @streamUser, args - - notifyUsersOfRoom: (room, eventName, args...) -> - console.log "RocketChat.Notifications: notifyUsersOfRoom", arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoomUsers.emit.apply @streamRoomUsers, args - - onAll: (eventName, callback) -> - @streamAll.on eventName, callback - - onLogged: (eventName, callback) -> - @onLogin => - @streamLogged.on eventName, callback - - onRoom: (room, eventName, callback) -> - if @debug is true - @streamRoom.on room, -> console.log "RocketChat.Notifications: onRoom #{room}", arguments - - @streamRoom.on "#{room}/#{eventName}", callback - - onUser: (eventName, callback) -> - @streamUser.on "#{Meteor.userId()}/#{eventName}", callback - - - unAll: (callback) -> - @streamAll.removeListener 'notify', callback - - unLogged: (callback) -> - @streamLogged.removeListener 'notify', callback - - unRoom: (room, eventName, callback) -> - @streamRoom.removeListener "#{room}/#{eventName}", callback - - unUser: (callback) -> - @streamUser.removeListener Meteor.userId(), callback diff --git a/packages/rocketchat-lib/client/Notifications.js b/packages/rocketchat-lib/client/Notifications.js new file mode 100644 index 000000000000..4a474c9aec24 --- /dev/null +++ b/packages/rocketchat-lib/client/Notifications.js @@ -0,0 +1,86 @@ +RocketChat.Notifications = new class { + constructor() { + this.logged = Meteor.userId() !== null; + this.loginCb = []; + Tracker.autorun(() => { + if (Meteor.userId() !== null && this.logged === false) { + this.loginCb.forEach(cb => cb()); + } + return this.logged = Meteor.userId() !== null; + }); + 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'); + if (this.debug === true) { + this.onAll(function() { + return console.log('RocketChat.Notifications: onAll', arguments); + }); + this.onUser(function() { + return console.log('RocketChat.Notifications: onAll', arguments); + }); + } + } + + onLogin(cb) { + this.loginCb.push(cb); + if (this.logged) { + return cb(); + } + } + notifyRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('RocketChat.Notifications: notifyRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emit.apply(this.streamRoom, args); + } + notifyUser(userId, eventName, ...args) { + if (this.debug === true) { + console.log('RocketChat.Notifications: notifyUser', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emit.apply(this.streamUser, args); + } + notifyUsersOfRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('RocketChat.Notifications: notifyUsersOfRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoomUsers.emit.apply(this.streamRoomUsers, args); + } + onAll(eventName, callback) { + return this.streamAll.on(eventName, callback); + } + onLogged(eventName, callback) { + return this.onLogin(() => { + return this.streamLogged.on(eventName, callback); + }); + } + onRoom(room, eventName, callback) { + if (this.debug === true) { + this.streamRoom.on(room, function() { + return console.log(`RocketChat.Notifications: onRoom ${ room }`, arguments); + }); + } + return this.streamRoom.on(`${ room }/${ eventName }`, callback); + } + onUser(eventName, callback) { + return this.streamUser.on(`${ Meteor.userId() }/${ eventName }`, callback); + } + unAll(callback) { + return this.streamAll.removeListener('notify', callback); + } + unLogged(callback) { + return this.streamLogged.removeListener('notify', callback); + } + unRoom(room, eventName, callback) { + return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback); + } + unUser(callback) { + return this.streamUser.removeListener(Meteor.userId(), callback); + } + +}; diff --git a/packages/rocketchat-lib/client/lib/openRoom.js b/packages/rocketchat-lib/client/lib/openRoom.js new file mode 100644 index 000000000000..b283b51c08ab --- /dev/null +++ b/packages/rocketchat-lib/client/lib/openRoom.js @@ -0,0 +1,98 @@ +/* globals fireGlobalEvent readMessage currentTracker*/ +currentTracker = undefined; + +function openRoom(type, name) { + Session.set('openedRoom', null); + + return Meteor.defer(() => + currentTracker = Tracker.autorun(function(c) { + const user = Meteor.user(); + if ((user && user.username == null) || user == null && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === false) { + BlazeLayout.render('main'); + return; + } + + if (RoomManager.open(type + name).ready() !== true) { + BlazeLayout.render('main', { modal: RocketChat.Layout.isEmbedded(), center: 'loading' }); + return; + } + if (currentTracker) { + currentTracker = undefined; + } + c.stop(); + + const room = RocketChat.roomTypes.findRoom(type, name, user); + if (room == null) { + if (type === 'd') { + Meteor.call('createDirectMessage', name, function(err) { + if (!err) { + RoomManager.close(type + name); + return openRoom('d', name); + } else { + Session.set('roomNotFound', {type, name}); + BlazeLayout.render('main', {center: 'roomNotFound'}); + return; + } + }); + } else { + Meteor.call('getRoomByTypeAndName', type, name, function(err, record) { + if (err) { + Session.set('roomNotFound', {type, name}); + return BlazeLayout.render('main', {center: 'roomNotFound'}); + } else { + delete record.$loki; + RocketChat.models.Rooms.upsert({ _id: record._id }, _.omit(record, '_id')); + RoomManager.close(type + name); + return openRoom(type, name); + } + }); + } + return; + } + + const mainNode = document.querySelector('.main-content'); + if (mainNode) { + for (const child of Array.from(mainNode.children)) { + if (child) { mainNode.removeChild(child); } + } + const roomDom = RoomManager.getDomOfRoom(type + name, room._id); + mainNode.appendChild(roomDom); + if (roomDom.classList.contains('room-container')) { + roomDom.querySelector('.messages-box > .wrapper').scrollTop = roomDom.oldScrollTop; + } + } + + Session.set('openedRoom', room._id); + + fireGlobalEvent('room-opened', _.omit(room, 'usernames')); + + Session.set('editRoomTitle', false); + RoomManager.updateMentionsMarksOfRoom(type + name); + Meteor.setTimeout(() => readMessage.readNow(), 2000); + // KonchatNotification.removeRoomNotification(params._id) + + if (Meteor.Device.isDesktop() && window.chatMessages && window.chatMessages[room._id] != null) { + setTimeout(() => $('.message-form .input-message').focus(), 100); + } + + // update user's room subscription + const sub = ChatSubscription.findOne({rid: room._id}); + if (sub && sub.open === false) { + Meteor.call('openRoom', room._id, function(err) { + if (err) { + return handleError(err); + } + }); + } + + if (FlowRouter.getQueryParam('msg')) { + const msg = { _id: FlowRouter.getQueryParam('msg'), rid: room._id }; + RoomHistoryManager.getSurroundingMessages(msg); + } + + return RocketChat.callbacks.run('enter-room', sub); + }) + ); +} +export { openRoom }; +this.openRoom = openRoom; diff --git a/packages/rocketchat-lib/client/lib/roomExit.coffee b/packages/rocketchat-lib/client/lib/roomExit.coffee deleted file mode 100644 index d7da9ed4a871..000000000000 --- a/packages/rocketchat-lib/client/lib/roomExit.coffee +++ /dev/null @@ -1,20 +0,0 @@ -@roomExit = -> - RocketChat.callbacks.run 'roomExit' - - BlazeLayout.render 'main', {center: 'none'} - - if currentTracker? - currentTracker.stop() - - mainNode = document.querySelector('.main-content') - if mainNode? - for child in mainNode.children - if child? - if child.classList.contains('room-container') - wrapper = child.querySelector('.messages-box > .wrapper') - if wrapper - if wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight - child.oldScrollTop = 10e10 - else - child.oldScrollTop = wrapper.scrollTop - mainNode.removeChild child diff --git a/packages/rocketchat-lib/client/lib/roomExit.js b/packages/rocketchat-lib/client/lib/roomExit.js new file mode 100644 index 000000000000..36f1260e3420 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/roomExit.js @@ -0,0 +1,31 @@ +/*globals currentTracker */ +this.roomExit = function() { + RocketChat.callbacks.run('roomExit'); + BlazeLayout.render('main', { + center: 'none' + }); + + if (typeof currentTracker !== 'undefined') { + currentTracker.stop(); + } + const mainNode = document.querySelector('.main-content'); + if (mainNode == null) { + return; + } + return [...mainNode.children].forEach(child => { + if (child == null) { + return; + } + if (child.classList.contains('room-container')) { + const wrapper = child.querySelector('.messages-box > .wrapper'); + if (wrapper) { + if (wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight) { + child.oldScrollTop = 10e10; + } else { + child.oldScrollTop = wrapper.scrollTop; + } + } + } + mainNode.removeChild(child); + }); +}; diff --git a/packages/rocketchat-lib/client/lib/roomTypes.coffee b/packages/rocketchat-lib/client/lib/roomTypes.coffee deleted file mode 100644 index afe19195bf91..000000000000 --- a/packages/rocketchat-lib/client/lib/roomTypes.coffee +++ /dev/null @@ -1,90 +0,0 @@ -RocketChat.roomTypes = new class roomTypesClient extends roomTypesCommon - checkCondition: (roomType) -> - return not roomType.condition? or roomType.condition() - - getTypes: -> - orderedTypes = [] - - _.sortBy(@roomTypesOrder, 'order').forEach (type) => - orderedTypes.push @roomTypes[type.identifier] - - return orderedTypes - - getIcon: (roomType) -> - return @roomTypes[roomType]?.icon - - getRoomName: (roomType, roomData) -> - return @roomTypes[roomType]?.roomName roomData - - getSecondaryRoomName: (roomType, roomData) -> - return @roomTypes[roomType]?.secondaryRoomName?(roomData) - - getIdentifiers: (except) -> - except = [].concat except - list = _.reject @roomTypesOrder, (t) -> return except.indexOf(t.identifier) isnt -1 - return _.map list, (t) -> return t.identifier - - getUserStatus: (roomType, roomId) -> - return @roomTypes[roomType]?.getUserStatus?(roomId) - - findRoom: (roomType, identifier, user) -> - return @roomTypes[roomType]?.findRoom identifier, user - - canSendMessage: (roomId) -> - return ChatSubscription.find({ rid: roomId }).count() > 0 - - readOnly: (roomId, user) -> - - fields = { ro: 1 } - - # if a user has been specified then we want to see if that user has been muted in the room - if user - fields.muted = 1 - - room = ChatRoom.findOne({ _id: roomId }, fields : fields) - - unless user - return room?.ro; - - userOwner = RoomRoles.findOne({ rid: roomId, "u._id": user._id, roles: 'owner' }, { fields: { _id: 1 } }) - - return room?.ro is true and Array.isArray(room?.muted) and room?.muted.indexOf(user.username) != -1 and !userOwner - - archived: (roomId) -> - fields = { archived: 1 } - - room = ChatRoom.findOne({ _id: roomId }, fields : fields) - - return room?.archived is true - - verifyCanSendMessage: (roomId) -> - room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 } }) - return if not room?.t? - - roomType = room.t - - return @roomTypes[roomType]?.canSendMessage roomId if @roomTypes[roomType]?.canSendMessage? - - return @canSendMessage roomId - - verifyShowJoinLink: (roomId) -> - room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 } }) - return if not room?.t? - - roomType = room.t - - if not @roomTypes[roomType]?.showJoinLink? - return false - - return @roomTypes[roomType].showJoinLink roomId - - getNotSubscribedTpl: (roomId) -> - room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 } }) - return if not room?.t? - - roomType = room.t - - if not @roomTypes[roomType]?.notSubscribedTpl? - return false - - return @roomTypes[roomType].notSubscribedTpl diff --git a/packages/rocketchat-lib/client/lib/roomTypes.js b/packages/rocketchat-lib/client/lib/roomTypes.js new file mode 100644 index 000000000000..9cfc41585961 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/roomTypes.js @@ -0,0 +1,115 @@ +import roomTypesCommon from '../../lib/roomTypesCommon'; + +RocketChat.roomTypes = new class extends roomTypesCommon { + checkCondition(roomType) { + return roomType.condition == null || roomType.condition(); + } + getTypes() { + return _.sortBy(this.roomTypesOrder, 'order').map((type) => this.roomTypes[type.identifier]); + } + getIcon(roomType) { + return this.roomTypes[roomType] && this.roomTypes[roomType].icon; + } + getRoomName(roomType, roomData) { + return this.roomTypes[roomType] && this.roomTypes[roomType].roomName && this.roomTypes[roomType].roomName(roomData); + } + getSecondaryRoomName(roomType, roomData) { + return this.roomTypes[roomType] && typeof this.roomTypes[roomType].secondaryRoomName === 'function' && this.roomTypes[roomType].secondaryRoomName(roomData); + } + getIdentifiers(e) { + const except = [].concat(e); + const list = _.reject(this.roomTypesOrder, (t) => except.indexOf(t.identifier) !== -1); + return _.map(list, (t) => t.identifier); + } + getUserStatus(roomType, roomId) { + this.roomTypes[roomType] && typeof this.roomTypes[roomType].getUserStatus === 'function' && this.roomTypes[roomType].getUserStatus(roomId); + } + findRoom(roomType, identifier, user) { + return this.roomTypes[roomType] && this.roomTypes[roomType].findRoom(identifier, user); + } + canSendMessage(roomId) { + return ChatSubscription.find({ + rid: roomId + }).count() > 0; + } + readOnly(roomId, user) { + const fields = { + ro: 1 + }; + if (user) { + fields.muted = 1; + } + const room = ChatRoom.findOne({ + _id: roomId + }, { + fields + }); + if (!user) { + return room && room.ro; + } + /* globals RoomRoles */ + const userOwner = RoomRoles.findOne({ + rid: roomId, + 'u._id': user._id, + roles: 'owner' + }, { + fields: { + _id: 1 + } + }); + return room && (room.ro === true && Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !userOwner); + } + archived(roomId) { + const fields = { + archived: 1 + }; + const room = ChatRoom.findOne({ + _id: roomId + }, { + fields + }); + return room && room.archived === true; + } + verifyCanSendMessage(roomId) { + const room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 }}); + + if (!room || !room.t) { + return; + } + + const roomType = room.t; + if (this.roomTypes[roomType] && this.roomTypes[roomType].canSendMessage) { + return this.roomTypes[roomType].canSendMessage(roomId); + } + return this.canSendMessage(roomId); + } + verifyShowJoinLink(roomId) { + const room = ChatRoom.findOne({ + _id: roomId + }, { + fields: { + t: 1 + } + }); + if (!room || !room.t) { + return; + } + const roomType = room.t; + if (this.roomTypes[roomType] && !this.roomTypes[roomType].showJoinLink) { + return false; + } + return this.roomTypes[roomType].showJoinLink(roomId); + } + getNotSubscribedTpl(roomId) { + const room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 }}); + if (!room || !room.t) { + return; + } + const roomType = room.t; + if (this.roomTypes[roomType] && !this.roomTypes[roomType].notSubscribedTpl) { + return false; + } + return this.roomTypes[roomType].notSubscribedTpl; + } + +}; diff --git a/packages/rocketchat-lib/client/lib/settings.coffee b/packages/rocketchat-lib/client/lib/settings.coffee deleted file mode 100644 index 38c08e546ba2..000000000000 --- a/packages/rocketchat-lib/client/lib/settings.coffee +++ /dev/null @@ -1,65 +0,0 @@ -### -# RocketChat.settings holds all packages settings -# @namespace RocketChat.settings -### - -RocketChat.settings.cachedCollection = new RocketChat.CachedCollection({ name: 'public-settings', eventType: 'onAll', userRelated: false }) -RocketChat.settings.collection = RocketChat.settings.cachedCollection.collection -RocketChat.settings.cachedCollection.init() - -RocketChat.settings.dict = new ReactiveDict 'settings' - -RocketChat.settings.get = (_id) -> - return RocketChat.settings.dict.get(_id) - -RocketChat.settings.init = -> - initialLoad = true - RocketChat.settings.collection.find().observe - added: (record) -> - Meteor.settings[record._id] = record.value - RocketChat.settings.dict.set record._id, record.value - RocketChat.settings.load record._id, record.value, initialLoad - changed: (record) -> - Meteor.settings[record._id] = record.value - RocketChat.settings.dict.set record._id, record.value - RocketChat.settings.load record._id, record.value, initialLoad - removed: (record) -> - delete Meteor.settings[record._id] - RocketChat.settings.dict.set record._id, undefined - RocketChat.settings.load record._id, undefined, initialLoad - initialLoad = false - -RocketChat.settings.init() - -Meteor.startup -> - if Meteor.isCordova is false - Tracker.autorun (c) -> - siteUrl = RocketChat.settings.get('Site_Url') - if not siteUrl or not Meteor.userId()? - return - - if RocketChat.authz.hasRole(Meteor.userId(), 'admin') is false or Meteor.settings.public.sandstorm - return c.stop() - - Meteor.setTimeout -> - if __meteor_runtime_config__.ROOT_URL isnt location.origin - currentUrl = location.origin + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX - swal - type: 'warning' - title: t('Warning') - text: t("The_setting_s_is_configured_to_s_and_you_are_accessing_from_s", t('Site_Url'), siteUrl, currentUrl) + '

' + t("Do_you_want_to_change_to_s_question", currentUrl) - showCancelButton: true - confirmButtonText: t('Yes') - cancelButtonText: t('Cancel') - closeOnConfirm: false - html: true - , -> - Meteor.call 'saveSetting', 'Site_Url', currentUrl, -> - swal - title: t('Saved') - type: 'success' - timer: 1000 - showConfirmButton: false - , 100 - - return c.stop() diff --git a/packages/rocketchat-lib/client/lib/settings.js b/packages/rocketchat-lib/client/lib/settings.js new file mode 100644 index 000000000000..0f0c946ecd81 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/settings.js @@ -0,0 +1,87 @@ + +/* +* RocketChat.settings holds all packages settings +* @namespace RocketChat.settings +*/ + +/* globals ReactiveDict*/ + +RocketChat.settings.cachedCollection = new RocketChat.CachedCollection({ + name: 'public-settings', + eventType: 'onAll', + userRelated: false +}); + +RocketChat.settings.collection = RocketChat.settings.cachedCollection.collection; + +RocketChat.settings.cachedCollection.init(); + +RocketChat.settings.dict = new ReactiveDict('settings'); + +RocketChat.settings.get = function(_id) { + return RocketChat.settings.dict.get(_id); +}; + +RocketChat.settings.init = function() { + let initialLoad = true; + RocketChat.settings.collection.find().observe({ + added(record) { + Meteor.settings[record._id] = record.value; + RocketChat.settings.dict.set(record._id, record.value); + return RocketChat.settings.load(record._id, record.value, initialLoad); + }, + changed(record) { + Meteor.settings[record._id] = record.value; + RocketChat.settings.dict.set(record._id, record.value); + return RocketChat.settings.load(record._id, record.value, initialLoad); + }, + removed(record) { + delete Meteor.settings[record._id]; + RocketChat.settings.dict.set(record._id, null); + return RocketChat.settings.load(record._id, null, initialLoad); + } + }); + return initialLoad = false; +}; + +RocketChat.settings.init(); + +Meteor.startup(function() { + if (Meteor.isCordova === true) { + return; + } + Tracker.autorun(function(c) { + const siteUrl = RocketChat.settings.get('Site_Url'); + if (!siteUrl || (Meteor.userId() == null)) { + return; + } + if (RocketChat.authz.hasRole(Meteor.userId(), 'admin') === false || Meteor.settings['public'].sandstorm) { + return c.stop(); + } + Meteor.setTimeout(function() { + if (__meteor_runtime_config__.ROOT_URL !== location.origin) { + const currentUrl = location.origin + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + swal({ + type: 'warning', + title: t('Warning'), + text: `${ t('The_setting_s_is_configured_to_s_and_you_are_accessing_from_s', t('Site_Url'), siteUrl, currentUrl) }

${ t('Do_you_want_to_change_to_s_question', currentUrl) }`, + showCancelButton: true, + confirmButtonText: t('Yes'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: true + }, function() { + Meteor.call('saveSetting', 'Site_Url', currentUrl, function() { + swal({ + title: t('Saved'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + }); + }); + } + }, 100); + return c.stop(); + }); +}); diff --git a/packages/rocketchat-lib/client/methods/sendMessage.coffee b/packages/rocketchat-lib/client/methods/sendMessage.coffee deleted file mode 100644 index 68cb0a2f8c6e..000000000000 --- a/packages/rocketchat-lib/client/methods/sendMessage.coffee +++ /dev/null @@ -1,26 +0,0 @@ -Meteor.methods - sendMessage: (message) -> - if not Meteor.userId() - return false - - if _.trim(message.msg) isnt '' - if isNaN(TimeSync.serverOffset()) - message.ts = new Date() - else - message.ts = new Date(Date.now() + TimeSync.serverOffset()) - - user = Meteor.user() - message.u = - _id: Meteor.userId() - username: user.username - - if RocketChat.settings.get('UI_Use_Real_Name') - message.u.name = user.name - - message.temp = true - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - RocketChat.promises.run('onClientMessageReceived', message).then (message) -> - ChatMessage.insert message - RocketChat.callbacks.run 'afterSaveMessage', message diff --git a/packages/rocketchat-lib/client/methods/sendMessage.js b/packages/rocketchat-lib/client/methods/sendMessage.js new file mode 100644 index 000000000000..85cce415c2a8 --- /dev/null +++ b/packages/rocketchat-lib/client/methods/sendMessage.js @@ -0,0 +1,22 @@ +Meteor.methods({ + sendMessage(message) { + if (!Meteor.userId() || _.trim(message.msg) === '') { + return false; + } + const user = Meteor.user(); + message.ts = isNaN(TimeSync.serverOffset()) ? new Date() : new Date(Date.now() + TimeSync.serverOffset()); + message.u = { + _id: Meteor.userId(), + username: user.username + }; + if (RocketChat.settings.get('UI_Use_Real_Name')) { + message.u.name = user.name; + } + message.temp = true; + message = RocketChat.callbacks.run('beforeSaveMessage', message); + RocketChat.promises.run('onClientMessageReceived', message).then(function(message) { + ChatMessage.insert(message); + return RocketChat.callbacks.run('afterSaveMessage', message); + }); + } +}); diff --git a/packages/rocketchat-lib/client/models/Uploads.coffee b/packages/rocketchat-lib/client/models/Uploads.coffee deleted file mode 100644 index 4572e735b99b..000000000000 --- a/packages/rocketchat-lib/client/models/Uploads.coffee +++ /dev/null @@ -1,3 +0,0 @@ -RocketChat.models.Uploads = new class extends RocketChat.models._Base - constructor: -> - @_initModel 'uploads' diff --git a/packages/rocketchat-lib/client/models/Uploads.js b/packages/rocketchat-lib/client/models/Uploads.js new file mode 100644 index 000000000000..eefe630c708c --- /dev/null +++ b/packages/rocketchat-lib/client/models/Uploads.js @@ -0,0 +1,7 @@ + +RocketChat.models.Uploads = new class extends RocketChat.models._Base { + constructor() { + super(); + this._initModel('uploads'); + } +}; diff --git a/packages/rocketchat-lib/client/models/_Base.coffee b/packages/rocketchat-lib/client/models/_Base.coffee deleted file mode 100644 index fb485723241a..000000000000 --- a/packages/rocketchat-lib/client/models/_Base.coffee +++ /dev/null @@ -1,44 +0,0 @@ -RocketChat.models._Base = class - _baseName: -> - return 'rocketchat_' - - _initModel: (name) -> - check name, String - - @model = new Mongo.Collection @_baseName() + name - - find: -> - return @model.find.apply @model, arguments - - findOne: -> - return @model.findOne.apply @model, arguments - - insert: -> - return @model.insert.apply @model, arguments - - update: -> - return @model.update.apply @model, arguments - - upsert: -> - return @model.upsert.apply @model, arguments - - remove: -> - return @model.remove.apply @model, arguments - - allow: -> - return @model.allow.apply @model, arguments - - deny: -> - return @model.deny.apply @model, arguments - - ensureIndex: -> - return - - dropIndex: -> - return - - tryEnsureIndex: -> - return - - tryDropIndex: -> - return diff --git a/packages/rocketchat-lib/client/models/_Base.js b/packages/rocketchat-lib/client/models/_Base.js new file mode 100644 index 000000000000..55623317be66 --- /dev/null +++ b/packages/rocketchat-lib/client/models/_Base.js @@ -0,0 +1,52 @@ +RocketChat.models._Base = class { + + _baseName() { + return 'rocketchat_'; + } + + _initModel(name) { + check(name, String); + return this.model = new Mongo.Collection(this._baseName() + name); + } + + find() { + return this.model.find.apply(this.model, arguments); + } + + findOne() { + return this.model.findOne.apply(this.model, arguments); + } + + insert() { + return this.model.insert.apply(this.model, arguments); + } + + update() { + return this.model.update.apply(this.model, arguments); + } + + upsert() { + return this.model.upsert.apply(this.model, arguments); + } + + remove() { + return this.model.remove.apply(this.model, arguments); + } + + allow() { + return this.model.allow.apply(this.model, arguments); + } + + deny() { + return this.model.deny.apply(this.model, arguments); + } + + ensureIndex() {} + + dropIndex() {} + + tryEnsureIndex() {} + + tryDropIndex() {} + +}; diff --git a/packages/rocketchat-lib/lib/callbacks.coffee b/packages/rocketchat-lib/lib/callbacks.coffee deleted file mode 100644 index d0e3feb8ff32..000000000000 --- a/packages/rocketchat-lib/lib/callbacks.coffee +++ /dev/null @@ -1,129 +0,0 @@ -# https://github.com/TelescopeJS/Telescope/blob/master/packages/telescope-lib/lib/callbacks.js - -### -# Callback hooks provide an easy way to add extra steps to common operations. -# @namespace RocketChat.callbacks -### -RocketChat.callbacks = {} - -if Meteor.isServer - RocketChat.callbacks.showTime = true - RocketChat.callbacks.showTotalTime = true -else - RocketChat.callbacks.showTime = false - RocketChat.callbacks.showTotalTime = false - -### -# Callback priorities -### -RocketChat.callbacks.priority = - HIGH: -1000 - MEDIUM: 0 - LOW: 1000 - -### -# Add a callback function to a hook -# @param {String} hook - The name of the hook -# @param {Function} callback - The callback function -### -RocketChat.callbacks.add = (hook, callback, priority, id) -> - # if callback array doesn't exist yet, initialize it - priority ?= RocketChat.callbacks.priority.MEDIUM - unless _.isNumber priority - priority = RocketChat.callbacks.priority.MEDIUM - callback.priority = priority - callback.id = id or Random.id() - RocketChat.callbacks[hook] ?= [] - - if RocketChat.callbacks.showTime is true - err = new Error - callback.stack = err.stack - - # if not id? - # console.log('Callback without id', callback.stack) - - # Avoid adding the same callback twice - for cb in RocketChat.callbacks[hook] - if cb.id is callback.id - return - - RocketChat.callbacks[hook].push callback - return - -### -# Remove a callback from a hook -# @param {string} hook - The name of the hook -# @param {string} id - The callback's id -### - -RocketChat.callbacks.remove = (hookName, id) -> - RocketChat.callbacks[hookName] = _.reject RocketChat.callbacks[hookName], (callback) -> - callback.id is id - return - -### -# Successively run all of a hook's callbacks on an item -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -# @returns {Object} Returns the item after it's been through all the callbacks for this hook -### - -RocketChat.callbacks.run = (hook, item, constant) -> - callbacks = RocketChat.callbacks[hook] - if !!callbacks?.length - if RocketChat.callbacks.showTotalTime is true - totalTime = 0 - - # if the hook exists, and contains callbacks to run - result = _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.callbacks.priority.MEDIUM).reduce (result, callback) -> - # console.log(callback.name); - if RocketChat.callbacks.showTime is true or RocketChat.callbacks.showTotalTime is true - time = Date.now() - - callbackResult = callback result, constant - - if RocketChat.callbacks.showTime is true or RocketChat.callbacks.showTotalTime is true - currentTime = Date.now() - time - totalTime += currentTime - if RocketChat.callbacks.showTime is true - if Meteor.isServer - RocketChat.statsTracker.timing('callbacks.time', currentTime, ["hook:#{hook}", "callback:#{callback.id}"]); - else - console.log String(currentTime), hook, callback.id, callback.stack?.split?('\n')[2]?.match(/\(.+\)/)?[0] - - return if typeof callbackResult == 'undefined' then result else callbackResult - , item - - if RocketChat.callbacks.showTotalTime is true - if Meteor.isServer - RocketChat.statsTracker.timing('callbacks.totalTime', totalTime, ["hook:#{hook}"]); - else - console.log hook+':', totalTime - - return result - else - # else, just return the item unchanged - return item - -### -# Successively run all of a hook's callbacks on an item, in async mode (only works on server) -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -### - -RocketChat.callbacks.runAsync = (hook, item, constant) -> - callbacks = RocketChat.callbacks[hook] - if Meteor.isServer and !!callbacks?.length - # use defer to avoid holding up client - Meteor.defer -> - # run all post submit server callbacks on post object successively - _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.callbacks.priority.MEDIUM).forEach (callback) -> - # console.log(callback.name); - callback item, constant - return - return - else - return item - return diff --git a/packages/rocketchat-lib/lib/callbacks.js b/packages/rocketchat-lib/lib/callbacks.js new file mode 100644 index 000000000000..86f56a2eea8a --- /dev/null +++ b/packages/rocketchat-lib/lib/callbacks.js @@ -0,0 +1,131 @@ +/* +* Callback hooks provide an easy way to add extra steps to common operations. +* @namespace RocketChat.callbacks +*/ + +RocketChat.callbacks = {}; + +if (Meteor.isServer) { + RocketChat.callbacks.showTime = true; + RocketChat.callbacks.showTotalTime = true; +} else { + RocketChat.callbacks.showTime = false; + RocketChat.callbacks.showTotalTime = false; +} + + +/* +* Callback priorities +*/ + +RocketChat.callbacks.priority = { + HIGH: -1000, + MEDIUM: 0, + LOW: 1000 +}; + + +/* +* Add a callback function to a hook +* @param {String} hook - The name of the hook +* @param {Function} callback - The callback function +*/ + +RocketChat.callbacks.add = function(hook, callback, priority, id) { + if (priority == null) { + priority = RocketChat.callbacks.priority.MEDIUM; + } + if (!_.isNumber(priority)) { + priority = RocketChat.callbacks.priority.MEDIUM; + } + callback.priority = priority; + callback.id = id || Random.id(); + RocketChat.callbacks[hook] = RocketChat.callbacks[hook] || []; + if (RocketChat.callbacks.showTime === true) { + const err = new Error; + callback.stack = err.stack; + } + if (RocketChat.callbacks[hook].find((cb) => cb.id === callback.id)) { + return; + } + RocketChat.callbacks[hook].push(callback); +}; + + +/* +* Remove a callback from a hook +* @param {string} hook - The name of the hook +* @param {string} id - The callback's id +*/ + +RocketChat.callbacks.remove = function(hookName, id) { + RocketChat.callbacks[hookName] = _.reject(RocketChat.callbacks[hookName], (callback) => callback.id === id); +}; + + +/* +* Successively run all of a hook's callbacks on an item +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +* @returns {Object} Returns the item after it's been through all the callbacks for this hook +*/ + +RocketChat.callbacks.run = function(hook, item, constant) { + const callbacks = RocketChat.callbacks[hook]; + if (callbacks && callbacks.length) { + let totalTime = 0; + const result = _.sortBy(callbacks, function(callback) { + return callback.priority || RocketChat.callbacks.priority.MEDIUM; + }).reduce(function(result, callback) { + let time = 0; + if (RocketChat.callbacks.showTime === true || RocketChat.callbacks.showTotalTime === true) { + time = Date.now(); + } + const callbackResult = callback(result, constant); + if (RocketChat.callbacks.showTime === true || RocketChat.callbacks.showTotalTime === true) { + const currentTime = Date.now() - time; + totalTime += currentTime; + if (RocketChat.callbacks.showTime === true) { + if (Meteor.isServer) { + RocketChat.statsTracker.timing('callbacks.time', currentTime, [`hook:${ hook }`, `callback:${ callback.id }`]); + } else { + let stack = callback.stack && typeof callback.stack.split === 'function' && callback.stack.split('\n'); + stack = stack && stack[2] && (stack[2].match(/\(.+\)/)||[])[0]; + console.log(String(currentTime), hook, callback.id, stack); + } + } + } + return (typeof callbackResult === 'undefined') ? result : callbackResult; + }, item); + if (RocketChat.callbacks.showTotalTime === true) { + if (Meteor.isServer) { + RocketChat.statsTracker.timing('callbacks.totalTime', totalTime, [`hook:${ hook }`]); + } else { + console.log(`${ hook }:`, totalTime); + } + } + return result; + } else { + return item; + } +}; + + +/* +* Successively run all of a hook's callbacks on an item, in async mode (only works on server) +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +*/ + +RocketChat.callbacks.runAsync = function(hook, item, constant) { + const callbacks = RocketChat.callbacks[hook]; + if (Meteor.isServer && callbacks && callbacks.length) { + Meteor.defer(function() { + _.sortBy(callbacks, (callback) => callback.priority || RocketChat.callbacks.priority.MEDIUM).forEach((callback) => callback(item, constant)); + }); + } else { + return item; + } +}; diff --git a/packages/rocketchat-lib/lib/promises.coffee b/packages/rocketchat-lib/lib/promises.coffee deleted file mode 100644 index a23ab907b738..000000000000 --- a/packages/rocketchat-lib/lib/promises.coffee +++ /dev/null @@ -1,93 +0,0 @@ -# https://github.com/TelescopeJS/Telescope/blob/master/packages/telescope-lib/lib/callbacks.js - -### -# Callback hooks provide an easy way to add extra steps to common operations. -# @namespace RocketChat.promises -### -RocketChat.promises = {} - -### -# Callback priorities -### -RocketChat.promises.priority = - HIGH: -1000 - MEDIUM: 0 - LOW: 1000 - -### -# Add a callback function to a hook -# @param {String} hook - The name of the hook -# @param {Function} callback - The callback function -### - -RocketChat.promises.add = (hook, callback, priority, id) -> - # if callback array doesn't exist yet, initialize it - priority ?= RocketChat.promises.priority.MEDIUM - unless _.isNumber priority - priority = RocketChat.promises.priority.MEDIUM - callback.priority = priority - callback.id = id or Random.id() - RocketChat.promises[hook] ?= [] - - # Avoid adding the same callback twice - for cb in RocketChat.promises[hook] - if cb.id is callback.id - return - - RocketChat.promises[hook].push callback - return - -### -# Remove a callback from a hook -# @param {string} hook - The name of the hook -# @param {string} id - The callback's id -### - -RocketChat.promises.remove = (hookName, id) -> - RocketChat.promises[hookName] = _.reject RocketChat.promises[hookName], (callback) -> - callback.id is id - return - -### -# Successively run all of a hook's callbacks on an item -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -# @returns {Object} Returns the item after it's been through all the callbacks for this hook -### - -RocketChat.promises.run = (hook, item, constant) -> - callbacks = RocketChat.promises[hook] - if !!callbacks?.length - # if the hook exists, and contains callbacks to run - callbacks = _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.promises.priority.MEDIUM) - return callbacks.reduce (previousPromise, callback) -> - return new Promise (resolve, reject) -> - previousPromise.then (result) -> - callback(result, constant).then(resolve, reject) - , Promise.resolve(item) - else - # else, just return the item unchanged - return Promise.resolve(item) - -### -# Successively run all of a hook's callbacks on an item, in async mode (only works on server) -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -### - -RocketChat.promises.runAsync = (hook, item, constant) -> - callbacks = RocketChat.promises[hook] - if Meteor.isServer and !!callbacks?.length - # use defer to avoid holding up client - Meteor.defer -> - # run all post submit server callbacks on post object successively - _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.promises.priority.MEDIUM).forEach (callback) -> - # console.log(callback.name); - callback item, constant - return - return - else - return item - return diff --git a/packages/rocketchat-lib/lib/promises.js b/packages/rocketchat-lib/lib/promises.js new file mode 100644 index 000000000000..1e040f172bc5 --- /dev/null +++ b/packages/rocketchat-lib/lib/promises.js @@ -0,0 +1,89 @@ + +/* +* Callback hooks provide an easy way to add extra steps to common operations. +* @namespace RocketChat.promises +*/ + +RocketChat.promises = {}; + + +/* +* Callback priorities +*/ + +RocketChat.promises.priority = { + HIGH: -1000, + MEDIUM: 0, + LOW: 1000 +}; + + +/* +* Add a callback function to a hook +* @param {String} hook - The name of the hook +* @param {Function} callback - The callback function +*/ + +RocketChat.promises.add = function(hook, callback, p = RocketChat.promises.priority.MEDIUM, id) { + const priority = !_.isNumber(p) ? RocketChat.promises.priority.MEDIUM : p; + callback.priority = priority; + callback.id = id || Random.id(); + RocketChat.promises[hook] = RocketChat.promises[hook] || []; + if (RocketChat.promises[hook].find(cb => cb.id === callback.id)) { + return; + } + RocketChat.promises[hook].push(callback); +}; + + +/* +* Remove a callback from a hook +* @param {string} hook - The name of the hook +* @param {string} id - The callback's id +*/ + +RocketChat.promises.remove = function(hookName, id) { + RocketChat.promises[hookName] = _.reject(RocketChat.promises[hookName], (callback) => callback.id === id); +}; + + +/* +* Successively run all of a hook's callbacks on an item +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +* @returns {Object} Returns the item after it's been through all the callbacks for this hook +*/ + +RocketChat.promises.run = function(hook, item, constant) { + let callbacks = RocketChat.promises[hook]; + if (callbacks == null || callbacks.length === 0) { + return Promise.resolve(item); + } + callbacks = _.sortBy(callbacks, (callback) => callback.priority || RocketChat.promises.priority.MEDIUM); + return callbacks.reduce(function(previousPromise, callback) { + return new Promise(function(resolve, reject) { + return previousPromise.then((result) => callback(result, constant).then(resolve, reject)); + }); + }, Promise.resolve(item)); +}; + + +/* +* Successively run all of a hook's callbacks on an item, in async mode (only works on server) +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +*/ + +RocketChat.promises.runAsync = function(hook, item, constant) { + const callbacks = RocketChat.promises[hook]; + if (!Meteor.isServer || callbacks == null || callbacks.length === 0) { + return item; + } + Meteor.defer(() => { + _.sortBy(callbacks, (callback) => callback.priority || RocketChat.promises.priority.MEDIUM).forEach(function(callback) { + callback(item, constant); + }); + }); +}; diff --git a/packages/rocketchat-lib/lib/roomTypesCommon.coffee b/packages/rocketchat-lib/lib/roomTypesCommon.coffee deleted file mode 100644 index a351dc258922..000000000000 --- a/packages/rocketchat-lib/lib/roomTypesCommon.coffee +++ /dev/null @@ -1,75 +0,0 @@ -class @roomTypesCommon - roomTypes: {} - roomTypesOrder: [] - mainOrder: 1 - - ### Adds a room type to app - @param identifier An identifier to the room type. If a real room, MUST BE the same of `db.rocketchat_room.t` field, if not, can be null - @param order Order number of the type - @param config - template: template name to render on sideNav - icon: icon class - route: - name: route name - action: route action function - ### - add: (identifier, order, config) -> - unless identifier? - identifier = Random.id() - - if @roomTypes[identifier]? - return false - - if not order? - order = @mainOrder + 10 - @mainOrder += 10 - - # @TODO validate config options - @roomTypesOrder.push - identifier: identifier - order: order - @roomTypes[identifier] = config - - if config.route?.path? and config.route?.name? and config.route?.action? - routeConfig = - name: config.route.name - action: config.route.action - - if Meteor.isClient - routeConfig.triggersExit = [ roomExit ] - - FlowRouter.route config.route.path, routeConfig - - hasCustomLink: (roomType) -> - return @roomTypes[roomType]?.route?.link? - - ### - @param roomType: room type (e.g.: c (for channels), d (for direct channels)) - @param subData: the user's subscription data - ### - getRouteLink: (roomType, subData) -> - unless @roomTypes[roomType]? - return false - - routeData = {} - - if @roomTypes[roomType]?.route?.link? - routeData = @roomTypes[roomType].route.link(subData) - else if subData?.name? - routeData = { name: subData.name } - - return FlowRouter.path @roomTypes[roomType].route.name, routeData - - openRouteLink: (roomType, subData, queryParams) -> - unless @roomTypes[roomType]? - return false - - routeData = {} - - if @roomTypes[roomType]?.route?.link? - routeData = @roomTypes[roomType].route.link(subData) - else if subData?.name? - routeData = { name: subData.name } - - return FlowRouter.go @roomTypes[roomType].route.name, routeData, queryParams - diff --git a/packages/rocketchat-lib/lib/roomTypesCommon.js b/packages/rocketchat-lib/lib/roomTypesCommon.js new file mode 100644 index 000000000000..1df20edae0b3 --- /dev/null +++ b/packages/rocketchat-lib/lib/roomTypesCommon.js @@ -0,0 +1,84 @@ +/* globals roomExit*/ +this.roomTypesCommon = class { + constructor() { + this.roomTypes = {}; + this.roomTypesOrder = []; + this.mainOrder = 1; + } + + /* Adds a room type to app + @param identifier An identifier to the room type. If a real room, MUST BE the same of `db.rocketchat_room.t` field, if not, can be null + @param order Order number of the type + @param config + template: template name to render on sideNav + icon: icon class + route: + name: route name + action: route action function + */ + + add(identifier = Random.id(), order, config) { + if (this.roomTypes[identifier] != null) { + return false; + } + if (order == null) { + order = this.mainOrder + 10; + this.mainOrder += 10; + } + this.roomTypesOrder.push({ + identifier, + order + }); + this.roomTypes[identifier] = config; + if (config.route && config.route.path && config.route.name && config.route.action) { + const routeConfig = { + name: config.route.name, + action: config.route.action + }; + if (Meteor.isClient) { + routeConfig.triggersExit = [roomExit]; + } + return FlowRouter.route(config.route.path, routeConfig); + } + } + + hasCustomLink(roomType) { + return this.roomTypes[roomType] && this.roomTypes[roomType].route && this.roomTypes[roomType].route.link != null; + } + + /* + @param roomType: room type (e.g.: c (for channels), d (for direct channels)) + @param subData: the user's subscription data + */ + + getRouteLink(roomType, subData) { + if (this.roomTypes[roomType] == null) { + return false; + } + let routeData = {}; + if (this.roomTypes[roomType] && this.roomTypes[roomType].route && this.roomTypes[roomType].route.link) { + routeData = this.roomTypes[roomType].route.link(subData); + } else if (subData && subData.name) { + routeData = { + name: subData.name + }; + } + return FlowRouter.path(this.roomTypes[roomType].route.name, routeData); + } + + openRouteLink(roomType, subData, queryParams) { + if (this.roomTypes[roomType] == null) { + return false; + } + let routeData = {}; + if (this.roomTypes[roomType] && this.roomTypes[roomType].route && this.roomTypes[roomType].route.link) { + routeData = this.roomTypes[roomType].route.link(subData); + } else if (subData && subData.name) { + routeData = { + name: subData.name + }; + } + return FlowRouter.go(this.roomTypes[roomType].route.name, routeData, queryParams); + } +}; +export default this.roomTypesCommon; diff --git a/packages/rocketchat-lib/lib/settings.coffee b/packages/rocketchat-lib/lib/settings.coffee deleted file mode 100644 index cdc221064b39..000000000000 --- a/packages/rocketchat-lib/lib/settings.coffee +++ /dev/null @@ -1,83 +0,0 @@ -### -# RocketChat.settings holds all packages settings -# @namespace RocketChat.settings -### -RocketChat.settings = - callbacks: {} - regexCallbacks: {} - ts: new Date - - get: (_id, callback) -> - if callback? - RocketChat.settings.onload _id, callback - if _id is '*' and Meteor.settings? - for key, value of Meteor.settings - callback key, value - return - - if _.isRegExp(_id) - for key, value of Meteor.settings when _id.test(key) - callback key, value - return - - if Meteor.settings?[_id]? - callback _id, Meteor.settings?[_id] - else - if _.isRegExp(_id) - items = [] - for key, value of Meteor.settings when _id.test(key) - items.push - key: key - value: value - return items - - return Meteor.settings?[_id] - - set: (_id, value, callback) -> - Meteor.call 'saveSetting', _id, value, callback - - batchSet: (settings, callback) -> - - # async -> sync - # http://daemon.co.za/2012/04/simple-async-with-only-underscore/ - - save = (setting) -> - return (callback) -> - Meteor.call 'saveSetting', setting._id, setting.value, setting.editor, callback - - actions = _.map settings, (setting) -> save(setting) - _(actions).reduceRight(_.wrap, (err, success) -> return callback err, success)() - - load: (key, value, initialLoad) -> - if RocketChat.settings.callbacks[key]? - for callback in RocketChat.settings.callbacks[key] - callback key, value, initialLoad - - if RocketChat.settings.callbacks['*']? - for callback in RocketChat.settings.callbacks['*'] - callback key, value, initialLoad - - for cbKey, cbValue of RocketChat.settings.regexCallbacks - if cbValue.regex.test(key) - callback(key, value, initialLoad) for callback in cbValue.callbacks - - - onload: (key, callback) -> - # if key is '*' - # for key, value in Meteor.settings - # callback key, value, false - # else if Meteor.settings?[_id]? - # callback key, Meteor.settings[_id], false - - keys = [].concat key - - for k in keys - if _.isRegExp k - RocketChat.settings.regexCallbacks[k.source] ?= { - regex: k - callbacks: [] - } - RocketChat.settings.regexCallbacks[k.source].callbacks.push callback - else - RocketChat.settings.callbacks[k] ?= [] - RocketChat.settings.callbacks[k].push callback diff --git a/packages/rocketchat-lib/lib/settings.js b/packages/rocketchat-lib/lib/settings.js new file mode 100644 index 000000000000..9c14943dac63 --- /dev/null +++ b/packages/rocketchat-lib/lib/settings.js @@ -0,0 +1,99 @@ + +/* +* RocketChat.settings holds all packages settings +* @namespace RocketChat.settings +*/ +RocketChat.settings = { + callbacks: {}, + regexCallbacks: {}, + ts: new Date, + get(_id, callback) { + if (callback != null) { + RocketChat.settings.onload(_id, callback); + if (!Meteor.settings) { + return; + } + if (_id === '*') { + return Object.keys(Meteor.settings).forEach(key => { + const value = Meteor.settings[key]; + callback(key, value); + }); + } + if (_.isRegExp(_id) && Meteor.settings) { + return Object.keys(Meteor.settings).forEach(key => { + if (!_id.test(key)) { + return; + } + const value = Meteor.settings[key]; + callback(key, value); + }); + } + return Meteor.settings[_id] != null && callback(_id, Meteor.settings[_id]); + } else { + if (!Meteor.settings) { + return; + } + if (_.isRegExp(_id)) { + return Object.keys(Meteor.settings).reduce((items, key) => { + const value = Meteor.settings[key]; + if (_id.test(key)) { + items.push({ + key, + value + }); + } + return items; + }, []); + } + return Meteor.settings && Meteor.settings[_id]; + } + }, + set(_id, value, callback) { + return Meteor.call('saveSetting', _id, value, callback); + }, + batchSet(settings, callback) { + // async -> sync + // http://daemon.co.za/2012/04/simple-async-with-only-underscore/ + const save = function(setting) { + return function(callback) { + return Meteor.call('saveSetting', setting._id, setting.value, setting.editor, callback); + }; + }; + const actions = _.map(settings, (setting) => save(setting)); + return _(actions).reduceRight(_.wrap, (err, success) => callback(err, success))(); + }, + load(key, value, initialLoad) { + ['*', key].forEach(key => { + if (RocketChat.settings.callbacks[key]) { + RocketChat.settings.callbacks[key].forEach(callback => callback(key, value, initialLoad)); + } + }); + Object.keys(RocketChat.settings.regexCallbacks).forEach(cbKey => { + const cbValue = RocketChat.settings.regexCallbacks[cbKey]; + if (!cbValue.regex.test(key)) { + return; + } + cbValue.callbacks.forEach(callback => callback(key, value, initialLoad)); + }); + }, + onload(key, callback) { + // if key is '*' + // for key, value in Meteor.settings + // callback key, value, false + // else if Meteor.settings?[_id]? + // callback key, Meteor.settings[_id], false + const keys = [].concat(key); + keys.forEach(k => { + if (_.isRegExp(k)) { + RocketChat.settings.regexCallbacks[name = k.source] = RocketChat.settings.regexCallbacks[name = k.source] || { + regex: k, + callbacks: [] + }; + RocketChat.settings.regexCallbacks[k.source].callbacks.push(callback); + } else { + RocketChat.settings.callbacks[k] = RocketChat.settings.callbacks[k] || []; + RocketChat.settings.callbacks[k].push(callback); + } + }); + } +}; diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index b09e0428bd3d..3d7f8402927a 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -52,12 +52,12 @@ Package.onUse(function(api) { // COMMON LIB api.addFiles('lib/getURL.js'); - api.addFiles('lib/settings.coffee'); - api.addFiles('lib/callbacks.coffee'); + api.addFiles('lib/settings.js'); + api.addFiles('lib/callbacks.js'); api.addFiles('lib/fileUploadRestrictions.js'); api.addFiles('lib/placeholders.js'); - api.addFiles('lib/promises.coffee'); - api.addFiles('lib/roomTypesCommon.coffee'); + api.addFiles('lib/promises.js'); + api.addFiles('lib/roomTypesCommon.js'); api.addFiles('lib/slashCommand.js'); api.addFiles('lib/Message.js'); api.addFiles('lib/MessageTypes.js'); @@ -174,29 +174,29 @@ Package.onUse(function(api) { api.addFiles('lib/startup/settingsOnLoadSiteUrl.js'); // CLIENT LIB - api.addFiles('client/Notifications.coffee', 'client'); + api.addFiles('client/Notifications.js', 'client'); api.addFiles('client/OAuthProxy.js', 'client'); api.addFiles('client/lib/TabBar.js', 'client'); api.addFiles('client/lib/RocketChatTabBar.js', 'client'); api.addFiles('client/lib/cachedCollection.js', 'client'); - api.addFiles('client/lib/openRoom.coffee', 'client'); - api.addFiles('client/lib/roomExit.coffee', 'client'); - api.addFiles('client/lib/settings.coffee', 'client'); - api.addFiles('client/lib/roomTypes.coffee', 'client'); + api.addFiles('client/lib/openRoom.js', 'client'); + api.addFiles('client/lib/roomExit.js', 'client'); + api.addFiles('client/lib/settings.js', 'client'); + api.addFiles('client/lib/roomTypes.js', 'client'); api.addFiles('client/lib/userRoles.js', 'client'); api.addFiles('client/lib/Layout.js', 'client'); // CLIENT METHODS - api.addFiles('client/methods/sendMessage.coffee', 'client'); + api.addFiles('client/methods/sendMessage.js', 'client'); api.addFiles('client/AdminBox.js', 'client'); - api.addFiles('client/MessageAction.coffee', 'client'); + api.addFiles('client/MessageAction.js', 'client'); api.addFiles('client/defaultTabBars.js', 'client'); api.addFiles('client/CustomTranslations.js', 'client'); // CLIENT MODELS - api.addFiles('client/models/_Base.coffee', 'client'); - api.addFiles('client/models/Uploads.coffee', 'client'); + api.addFiles('client/models/_Base.js', 'client'); + api.addFiles('client/models/Uploads.js', 'client'); // CLIENT VIEWS api.addFiles('client/views/customFieldsForm.html', 'client');