diff --git a/client/lib/tapi18n.coffee b/client/lib/tapi18n.coffee index 1cf705e3952..b93b8f8193a 100644 --- a/client/lib/tapi18n.coffee +++ b/client/lib/tapi18n.coffee @@ -13,3 +13,6 @@ @isRtl = (language) -> # https://en.wikipedia.org/wiki/Right-to-left#cite_note-2 return language?.split('-').shift().toLowerCase() in ['ar', 'dv', 'fa', 'he', 'ku', 'ps', 'sd', 'ug', 'ur', 'yi'] + +UI.registerHelper '_t', (key) -> + return TAPi18next.t key \ No newline at end of file diff --git a/client/stylesheets/base.less b/client/stylesheets/base.less index 0d1830fc5a3..c44cd969208 100644 --- a/client/stylesheets/base.less +++ b/client/stylesheets/base.less @@ -2375,15 +2375,87 @@ a.github-fork { cursor: pointer; } &:hover:not(.system) .edit-message { - display: inline-block; + display: block; } .delete-message { display: none; cursor: pointer; } &:hover:not(.system) .delete-message { + display: block; + } + .message-cog-container { + position: relative; display: inline-block; + .message-cog { + visibility: hidden; + cursor: pointer; + } + } + &:hover:not(.system) .message-cog { + visibility: visible; } + + @keyframes dropdown-in { + 0% { + display: none; + opacity: 0; + } + + 1% { + display: block; + opacity: 0; + transform: scale(0); + } + + 100% { + opacity: 1; + transform: scale(1); + } + } + + .message-dropdown { + position: absolute; + top: -5px; + left: -2px; + z-index: 1000; + display: none; + background-color: #fff; + border: 1px solid #eee; + border-radius: 4px; + overflow: hidden; + box-shadow: 1px 1px 4px #B3B3B3; + transition: transform .15s ease-in-out, opacity .15s ease-in-out; + animation: dropdown-in .15s ease-in-out; + + ul { + display: flex; + display: -webkit-flex; + padding: 0px; + font-size: 14px; + + li { + display: block; + padding: 0px 8px; + font-weight: 400; + line-height: 26px; + cursor: pointer; + color: #666; + &:first-child { + padding-left: 6px; + background-color: #f8f8f8; + border-right: 1px solid #eee; + } + &:last-child { + padding-right: 13px; + } + &:hover { + background-color: #eee; + } + } + } + } + .user { display: inline-block; font-weight: 600; @@ -2437,6 +2509,12 @@ a.github-fork { float: left; } } + + // .message-dropdown { + // top: 100%; + // left: 0; + // } + &:hover { .time { display: inline-block; diff --git a/client/views/app/message.coffee b/client/views/app/message.coffee index bdd7fb52b13..75764bda858 100644 --- a/client/views/app/message.coffee +++ b/client/views/app/message.coffee @@ -1,5 +1,7 @@ Template.message.helpers - + actions: -> + return RocketChat.MessageAction.getButtons(this) + own: -> return 'own' if this.u?._id is Meteor.userId() diff --git a/client/views/app/message.html b/client/views/app/message.html index 70e01485246..2ff13ac47cd 100644 --- a/client/views/app/message.html +++ b/client/views/app/message.html @@ -10,14 +10,21 @@ {{#if private}} {{_ "Only_you_can_see_this_message"}} {{/if}} - - {{#if canEdit}} - - {{/if}} - {{#if canDelete}} - + {{#if actions.length}} +
+ +
+ +
+
{{/if}} +
{{{body}}}
diff --git a/client/views/app/room.coffee b/client/views/app/room.coffee index d71bf3f5407..0c6b3efa500 100644 --- a/client/views/app/room.coffee +++ b/client/views/app/room.coffee @@ -405,12 +405,13 @@ Template.room.events 'click .see-all': (e, instance) -> instance.showUsersOffline.set(!instance.showUsersOffline.get()) - "click .edit-message": (e) -> - Template.instance().chatMessages.edit(e.currentTarget.parentNode.parentNode) - input = Template.instance().find('.input-message') - Meteor.setTimeout -> - input.focus() - , 200 + 'click .message-cog': (e) -> + message_id = $(e.currentTarget).closest('.message').attr('id') + $('.message-dropdown:visible').hide() + $("\##{message_id} .message-dropdown").show() + + 'click .message-dropdown-close': -> + $('.message-dropdown:visible').hide() "click .editing-commands-cancel > a": (e) -> Template.instance().chatMessages.clearEditing() @@ -431,30 +432,6 @@ Template.room.events 'click .image-to-download': (event) -> ChatMessage.update {_id: this._arguments[1]._id, 'urls.url': $(event.currentTarget).data('url')}, {$set: {'urls.$.downloadImages': true}} - 'click .delete-message': (event) -> - message = @_arguments[1] - msg = event.currentTarget.parentNode.parentNode - instance = Template.instance() - return if msg.classList.contains("system") - 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 - - instance.chatMessages.deleteMsg(message) 'click .pin-message': (event) -> message = @_arguments[1] instance = Template.instance() @@ -569,6 +546,12 @@ Template.room.onCreated -> @autorun -> self.subscribe 'fullUserData', Session.get('showUserInfo'), 1 + for button in RocketChat.MessageAction.getButtons() + if _.isFunction button.action + evt = {} + evt["click .#{button.id}"] = button.action + Template.room.events evt + Template.room.onDestroyed -> RocketChat.TabBar.resetButtons() diff --git a/packages/rocketchat-ldap/package.js b/packages/rocketchat-ldap/package.js index 8333f584058..0d510d5fdff 100644 --- a/packages/rocketchat-ldap/package.js +++ b/packages/rocketchat-ldap/package.js @@ -12,11 +12,11 @@ Npm.depends({ // Loads all i18n.json files into tapi18nFiles var _ = Npm.require('underscore'); var fs = Npm.require('fs'); -tapi18nFiles = fs.readdirSync('packages/rocketchat-ldap/i18n').forEach(function(filename) { +tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-ldap/i18n'), function(filename) { if (fs.statSync('packages/rocketchat-ldap/i18n/' + filename).size > 16) { return 'i18n/' + filename; } -}); +})); Package.onUse(function(api) { api.versionsFrom('1.0.3.1'); @@ -35,7 +35,6 @@ Package.onUse(function(api) { // Common // TAP api.addFiles('package-tap.i18n'); - api.addFiles(tapi18nFiles); // Client api.addFiles('ldap_client.js', 'client'); @@ -43,6 +42,7 @@ Package.onUse(function(api) { api.addFiles('ldap_server.js', 'server'); api.addFiles('config_server.coffee', 'server'); + api.addFiles(tapi18nFiles); api.export('LDAP', 'server'); api.export('LDAP_DEFAULTS', 'server'); diff --git a/packages/rocketchat-lib/client/MessageAction.coffee b/packages/rocketchat-lib/client/MessageAction.coffee new file mode 100644 index 00000000000..d43de43796a --- /dev/null +++ b/packages/rocketchat-lib/client/MessageAction.coffee @@ -0,0 +1,104 @@ +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 + throw new Meteor.Error "MessageAction-addButton-error", "Button id was not informed." + + 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 + + getButtons = (message) -> + allButtons = _.toArray buttons.get() + if message + allowedButtons = _.compact _.map allButtons, (button) -> + unless button.validation? + return true + if button.validation(message) + return button + else + allowedButtons = allButtons + + return _.sortBy allowedButtons, 'order' + + resetButtons = -> + buttons.set {} + + addButton: addButton + removeButton: removeButton + updateButton: updateButton + getButtons: getButtons + resetButtons: resetButtons + +Meteor.startup -> + RocketChat.MessageAction.addButton + id: 'edit-message' + icon: 'icon-pencil' + i18nLabel: 'rocketchat-lib:Edit' + action: (event, instance) -> + message = $(event.currentTarget).closest('.message')[0] + instance.chatMessages.edit(message) + $("\##{message.id} .message-dropdown").hide() + input = instance.find('.input-message') + Meteor.setTimeout -> + input.focus() + , 200 + validation: (message) -> + return RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid ) or RocketChat.settings.get('Message_AllowEditing') and message.u?._id is Meteor.userId() + order: 1 + + RocketChat.MessageAction.addButton + id: 'delete-message' + icon: 'icon-trash-1' + i18nLabel: 'rocketchat-lib:Delete' + action: (event, instance) -> + message = @_arguments[1] + msg = $(event.currentTarget).closest('.message')[0] + $("\##{msg.id} .message-dropdown").hide() + return if msg.classList.contains("system") + 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 + + instance.chatMessages.deleteMsg(message) + validation: (message) -> + return RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid ) or RocketChat.settings.get('Message_AllowDeleting') and message.u?._id is Meteor.userId() + order: 2 \ No newline at end of file diff --git a/packages/rocketchat-lib/i18n/en.i18n.json b/packages/rocketchat-lib/i18n/en.i18n.json new file mode 100644 index 00000000000..9c76e8e402a --- /dev/null +++ b/packages/rocketchat-lib/i18n/en.i18n.json @@ -0,0 +1,4 @@ +{ + "Edit": "Edit", + "Delete": "Delete" +} \ No newline at end of file diff --git a/packages/rocketchat-lib/i18n/pt.i18n.json b/packages/rocketchat-lib/i18n/pt.i18n.json new file mode 100644 index 00000000000..a5227168f1f --- /dev/null +++ b/packages/rocketchat-lib/i18n/pt.i18n.json @@ -0,0 +1,4 @@ +{ + "Edit": "Editar", + "Delete": "Excluir" +} \ No newline at end of file diff --git a/packages/rocketchat-lib/package-tap.i18n b/packages/rocketchat-lib/package-tap.i18n new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 3c3d6334a6a..e3d524219e2 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -15,6 +15,19 @@ Package.onUse(function(api) { api.use('underscore'); api.use('underscorestring:underscore.string'); + // TAPi18n + api.use('templating', 'client'); + var _ = Npm.require('underscore'); + var fs = Npm.require('fs'); + tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-lib/i18n'), function(filename) { + if (fs.statSync('packages/rocketchat-lib/i18n/' + filename).size > 16) { + return 'i18n/' + filename; + } + })); + api.use(["tap:i18n@1.5.1"], ["client", "server"]); + api.imply('tap:i18n'); + api.addFiles("package-tap.i18n", ["client", "server"]); + // COMMON api.addFiles('lib/core.coffee'); api.addFiles('lib/callbacks.coffee'); @@ -23,15 +36,14 @@ Package.onUse(function(api) { api.addFiles('settings/lib/settings.coffee'); api.addFiles('settings/lib/rocketchat.coffee'); - // CLIENT api.addFiles('client/Notifications.coffee', 'client'); api.addFiles('client/TabBar.coffee', 'client'); + api.addFiles('client/MessageAction.coffee', 'client'); api.addFiles('settings/client/startup.coffee', 'client'); api.addFiles('settings/client/rocketchat.coffee', 'client'); - // SERVER api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server'); api.addFiles('server/functions/setUsername.coffee', 'server'); @@ -46,6 +58,7 @@ Package.onUse(function(api) { api.addFiles('server/Notifications.coffee', 'server'); + // Settings api.addFiles('settings/server/methods.coffee', 'server'); api.addFiles('settings/server/publication.coffee', 'server'); api.addFiles('settings/server/startup.coffee', 'server'); @@ -59,6 +72,8 @@ Package.onUse(function(api) { api.addFiles('server/models/Subscriptions.coffee', 'server'); api.addFiles('server/models/Rooms.coffee', 'server'); + // TAPi18n -- needs to be added last + api.addFiles(tapi18nFiles, ["client", "server"]); // EXPORT api.export('RocketChat'); diff --git a/packages/rocketchat-webrtc/webrtc.js b/packages/rocketchat-webrtc/webrtc.js index dfc03dace1b..c9188e1ffb4 100644 --- a/packages/rocketchat-webrtc/webrtc.js +++ b/packages/rocketchat-webrtc/webrtc.js @@ -3,6 +3,10 @@ webrtc = { pc: undefined, to: undefined, room: undefined, + activeMediastream: undefined, + remoteDataSDP: undefined, + mode: undefined, + lastSeenTimestamp: new Date(), debug: false, config: { iceServers: [ @@ -14,17 +18,28 @@ webrtc = { data.to = webrtc.to; data.room = webrtc.room; data.from = Meteor.user().username; + data.mod = (webrtc.mode ? webrtc.mode : 0); RocketChat.Notifications.notifyUser(data.to, 'webrtc', data); }, stop: function(sendEvent) { + if (webrtc.activeMediastream) { + webrtc.activeMediastream = undefined; + } + if (webrtc.pc) { if (webrtc.pc.signalingState != 'closed') { webrtc.pc.close(); - } - if (sendEvent != false) { - RocketChat.Notifications.notifyUser(webrtc.to, 'webrtc', {to: webrtc.to, room: webrtc.room, from: Meteor.userId(), close: true}); + webrtc.pc = undefined; + webrtc.mode = 0; } } + + + this.onRemoteUrl(); + this.onSelfUrl(); + if (sendEvent != false) { + RocketChat.Notifications.notifyUser(webrtc.to, 'webrtc', {to: webrtc.to, room: webrtc.room, from: Meteor.userId(), close: true}); + } }, log: function() { if (webrtc.debug === true) { @@ -39,6 +54,18 @@ function onError() { console.log(arguments); } +webrtc.activateLocalStream = function() { + var media ={ "audio": true, "video": {mandatory: {minWidth:1280, minHeight:720}}} ; + + // get the local stream, show it in the local video element and send it + navigator.getUserMedia(media, function (stream) { + webrtc.log('getUserMedia got stream'); + webrtc.onSelfUrl(URL.createObjectURL(stream)); + webrtc.activeMediastream = stream; + + }, function(e) { webrtc.log('getUserMedia failed during activateLocalStream ' + e); }); +} + // run start(true) to initiate a call webrtc.start = function (isCaller, fromUsername) { webrtc.pc = new RTCPeerConnection(webrtc.config); @@ -62,7 +89,7 @@ webrtc.start = function (isCaller, fromUsername) { // once remote stream arrives, show it in the remote video element webrtc.pc.onaddstream = function (evt) { - webrtc.log('onaddstream', arguments) + webrtc.log('onaddstream', arguments); webrtc.onRemoteUrl(URL.createObjectURL(evt.stream)); }; @@ -70,23 +97,53 @@ webrtc.start = function (isCaller, fromUsername) { webrtc.log('oniceconnectionstatechange', arguments) var srcElement = evt.srcElement || evt.target; if (srcElement.iceConnectionState == 'disconnected' || srcElement.iceConnectionState == 'closed') { - webrtc.pc.getLocalStreams().forEach(function(stream) { - stream.stop(); - webrtc.onSelfUrl(); - }); - webrtc.pc.getRemoteStreams().forEach(function(stream) { - if (stream.stop) { + if (webrtc.pc) { + webrtc.pc.getLocalStreams().forEach(function(stream) { stream.stop(); - } - webrtc.onRemoteUrl(); - }); - webrtc.pc = undefined; + webrtc.onSelfUrl(); + }); + webrtc.pc.getRemoteStreams().forEach(function(stream) { + if (stream.stop) { + stream.stop(); + } + webrtc.onRemoteUrl(); + }); + webrtc.pc = undefined; + webrtc.mode = 0; + } + } } - var getUserMedia = function() { + + var gotDescription = function(desc) { + webrtc.pc.setLocalDescription(desc, function() {}, onError); + webrtc.send({ "sdp": desc.toJSON(), cid: webrtc.cid }); + + } + + var CreateMonitoringOffer = function() { + + webrtc.pc.createOffer(gotDescription, onError, { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } }); + + } + + var AutoConnectStream = function() { + webrtc.pc.addStream(webrtc.activeMediastream); + webrtc.pc.setRemoteDescription(new RTCSessionDescription(webrtc.remoteDataSDP)); + webrtc.pc.createAnswer(gotDescription, onError); + + } + var LocalGetUserMedia = function() { + + + + var media ={ "audio": true, "video": {mandatory: {minWidth:1280, minHeight:720}}} ; + + // get the local stream, show it in the local video element and send it - navigator.getUserMedia({ "audio": true, "video": {mandatory: {minWidth:1280, minHeight:720}} }, function (stream) { + navigator.getUserMedia(media, function (stream) { + webrtc.log('getUserMedia got stream'); webrtc.onSelfUrl(URL.createObjectURL(stream)); webrtc.pc.addStream(stream); @@ -97,36 +154,49 @@ webrtc.start = function (isCaller, fromUsername) { webrtc.pc.createAnswer(gotDescription, onError); } - function gotDescription(desc) { - webrtc.pc.setLocalDescription(desc, function() {}, onError); - webrtc.send({ "sdp": desc.toJSON(), cid: webrtc.cid }); - } - }, function() {}); + }, function(e) { webrtc.log('getUserMedia failed' + e); }); + } if (isCaller) { - getUserMedia(); - } else { - swal({ - title: "Video call from "+fromUsername, - text: "Do you want to accept?", - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: "Yes", - cancelButtonText: "No" - }, function(isConfirm){ - if (isConfirm) { - getUserMedia(); - } else { - webrtc.stop(); + webrtc.log('isCaller LocalGetUserMedia'); + if (webrtc.mode) { + if (webrtc.mode === 2) { + CreateMonitoringOffer(); + } else { // node === 1 + + LocalGetUserMedia(); } - }); + } else { + // no mode + LocalGetUserMedia(); + } + + } else { + if (!webrtc.activeMediastream) { + swal({ + title: "Video call from "+fromUsername, + text: "Do you want to accept?", + type: "warning", + showCancelButton: true, + confirmButtonColor: "#DD6B55", + confirmButtonText: "Yes", + cancelButtonText: "No" + }, function(isConfirm){ + if (isConfirm) { + LocalGetUserMedia(); + } else { + webrtc.stop(); + } + }); + } else { + AutoConnectStream(); + } } } RocketChat.Notifications.onUser('webrtc', function(data) { - webrtc.log('stream.on', Meteor.userId(), data) + webrtc.log('processIncomingRtcMessage()', Meteor.userId(), data) if (!webrtc.to) { webrtc.to = data.room.replace(Meteor.userId(), ''); } @@ -135,15 +205,38 @@ RocketChat.Notifications.onUser('webrtc', function(data) { webrtc.room = data.room; } - if (data.close == true) { + + // do not stop local video if in monitoring mode + if (data.close == true) { + + if (webrtc.activeMediastream) { + if (webrtc.pc) { + webrtc.pc.getRemoteStreams().forEach(function(stream) { + if (!stream.stop) { + stream.stop(); + } + }); + webrtc.pc = undefined; + webrtc.mode = 0; + } + + } else { + webrtc.stop(false); - return + + } + return } - if (!webrtc.pc) + + if (!webrtc.pc) { + if ((webrtc.activeMediastream) && (data.sdp != undefined)){ + webrtc.remoteDataSDP = data.sdp; + } webrtc.start(false, data.from); + } - if (data.sdp) { + if (data.sdp != undefined) { webrtc.pc.setRemoteDescription(new RTCSessionDescription(data.sdp)); } else { if( ["closed", "failed", "disconnected", "completed"].indexOf(webrtc.pc.iceConnectionState) === -1) { diff --git a/server/lib/accounts.coffee b/server/lib/accounts.coffee index bf6b440764b..c359823ebc4 100644 --- a/server/lib/accounts.coffee +++ b/server/lib/accounts.coffee @@ -24,9 +24,6 @@ Accounts.onCreateUser (options, user) -> user.status = 'offline' user.active = not RocketChat.settings.get 'Accounts_ManuallyApproveNewUsers' - # when inserting first user give them admin privileges otherwise make a regular user - roleName = if RocketChat.models.Users.findOne() then 'user' else 'admin' - if not user?.name? or user.name is '' if options.profile?.name? user.name = options.profile?.name @@ -45,14 +42,20 @@ Accounts.onCreateUser (options, user) -> verified: true ] - Meteor.defer -> - # need to defer role assignment because underlying alanning:roles requires user - # to exist in users collection - RocketChat.authz.addUsersToRoles( user._id, roleName) - RocketChat.callbacks.run 'afterCreateUser', options, user - return user +# Wrap insertUserDoc to allow executing code after Accounts.insertUserDoc is run +Accounts.insertUserDoc = _.wrap Accounts.insertUserDoc, (insertUserDoc) -> + options = arguments[1] + user = arguments[2] + _id = insertUserDoc(options, user) + + # when inserting first user give them admin privileges otherwise make a regular user + roleName = if RocketChat.models.Users.findOne() then 'user' else 'admin' + + RocketChat.authz.addUsersToRoles(_id, roleName) + RocketChat.callbacks.run 'afterCreateUser', options, user + return _id Accounts.validateLoginAttempt (login) -> login = RocketChat.callbacks.run 'beforeValidateLogin', login