diff --git a/.meteor/packages b/.meteor/packages index c029bd1040c..d96e79316f8 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -70,7 +70,6 @@ pauli:accounts-linkedin percolate:migrations percolatestudio:synced-cron raix:handlebar-helpers -raix:push raix:ui-dropped-event tap:i18n tmeasday:crypto-md5 @@ -80,3 +79,4 @@ underscorestring:underscore.string yasaricli:slugify yasinuslu:blaze-meta rocketchat:colors +raix:push@2.6.13-rc.1 diff --git a/.meteor/versions b/.meteor/versions index a63b50c1dd4..21a0e79ba71 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -103,7 +103,7 @@ qnub:emojione@0.0.3 raix:eventemitter@0.1.3 raix:eventstate@0.0.2 raix:handlebar-helpers@0.2.4 -raix:push@2.6.12 +raix:push@2.6.13-rc.1 raix:ui-dropped-event@0.0.7 random@1.0.3 reactive-dict@1.1.0 diff --git a/.travis.yml b/.travis.yml index 79c27a1a335..f51f1ee5888 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,6 @@ before_install: - "curl https://install.meteor.com | /bin/sh" script: - - meteor build --server rocket.chat ./ \ No newline at end of file + - meteor add rocketchat:external + - meteor add rocketchat:hubot + - meteor build --server demo.rocket.chat ./ \ No newline at end of file diff --git a/client/lib/cordova/facebook-login.coffee b/client/lib/cordova/facebook-login.coffee index af22e8a5ac1..5b7b7b771a7 100644 --- a/client/lib/cordova/facebook-login.coffee +++ b/client/lib/cordova/facebook-login.coffee @@ -16,12 +16,14 @@ Meteor.loginWithFacebookCordova = (options, callback) -> facebookConnectPlugin.getLoginStatus (response) -> if response.status isnt "connected" facebookConnectPlugin.login ["public_profile", "email"], fbLoginSuccess, (error) -> - console.log("" + error) + console.log('login', JSON.stringify(error), error) + callback(error) else fbLoginSuccess(response) , (error) -> - console.log("" + error) + console.log('getLoginStatus', JSON.stringify(error), error) + callback(error) else Facebook.requestCredential(options, credentialRequestCompleteCallback) \ No newline at end of file diff --git a/client/lib/fileUpload.coffee b/client/lib/fileUpload.coffee new file mode 100644 index 00000000000..bbe2cd79bf1 --- /dev/null +++ b/client/lib/fileUpload.coffee @@ -0,0 +1,59 @@ +@fileUpload = (files) -> + files = [].concat files + + consume = -> + file = files.pop() + if not file? + swal.close() + return + + reader = new FileReader() + reader.onload = (event) -> + fileContent = event.target.result + + text = '' + + if file.type is 'audio' + text = """ +
+ +
+
#{file.name}
+ """ + else + text = """ +
+
+
+
#{file.name}
+ """ + + swal + title: t('Upload_file_question') + text: text + showCancelButton: true + closeOnConfirm: false + closeOnCancel: false + html: true + , (isConfirm) -> + consume() + + if isConfirm isnt true + return + + newFile = new (FS.File)(file.file) + if file.name? + newFile.name(file.name) + newFile.rid = Session.get('openedRoom') + newFile.recId = Random.id() + newFile.userId = Meteor.userId() + Files.insert newFile, (error, fileObj) -> + unless error + toastr.success 'Upload succeeded!' + + reader.readAsDataURL(file.file) + + consume() \ No newline at end of file diff --git a/client/lib/recorderjs/audioRecored.coffee b/client/lib/recorderjs/audioRecored.coffee new file mode 100644 index 00000000000..c2cad918796 --- /dev/null +++ b/client/lib/recorderjs/audioRecored.coffee @@ -0,0 +1,40 @@ +@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 + + @audio_context = 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 = @audio_context.createMediaStreamSource(stream) + @recorder = new Recorder(input, {workerPath: '/recorderWorker.js'}) + @recorder.record() + + stop: (cb) -> + @recorder.stop() + + if cb? + @getBlob cb + + @stream.stop() + + @recorder.clear() + + delete @audio_context + delete @recorder + delete @stream + + getBlob: (cb) -> + @recorder.exportWAV cb \ No newline at end of file diff --git a/client/lib/recorderjs/recorder.js b/client/lib/recorderjs/recorder.js new file mode 100644 index 00000000000..e150e6a6088 --- /dev/null +++ b/client/lib/recorderjs/recorder.js @@ -0,0 +1,92 @@ +(function(window){ + + var WORKER_PATH = 'recorderWorker.js'; + + var Recorder = function(source, cfg){ + var config = cfg || {}; + var bufferLen = config.bufferLen || 4096; + var numChannels = config.numChannels || 2; + this.context = source.context; + this.node = (this.context.createScriptProcessor || + this.context.createJavaScriptNode).call(this.context, + bufferLen, numChannels, numChannels); + var worker = new Worker(config.workerPath || WORKER_PATH); + worker.postMessage({ + command: 'init', + config: { + sampleRate: this.context.sampleRate, + numChannels: numChannels + } + }); + var recording = false, + currCallback; + + this.node.onaudioprocess = function(e){ + if (!recording) return; + var buffer = []; + for (var channel = 0; channel < numChannels; channel++){ + buffer.push(e.inputBuffer.getChannelData(channel)); + } + worker.postMessage({ + command: 'record', + buffer: buffer + }); + } + + this.configure = function(cfg){ + for (var prop in cfg){ + if (cfg.hasOwnProperty(prop)){ + config[prop] = cfg[prop]; + } + } + } + + this.record = function(){ + recording = true; + } + + this.stop = function(){ + recording = false; + } + + this.clear = function(){ + worker.postMessage({ command: 'clear' }); + } + + this.getBuffer = function(cb) { + currCallback = cb || config.callback; + worker.postMessage({ command: 'getBuffer' }) + } + + this.exportWAV = function(cb, type){ + currCallback = cb || config.callback; + type = type || config.type || 'audio/wav'; + if (!currCallback) throw new Error('Callback not set'); + worker.postMessage({ + command: 'exportWAV', + type: type + }); + } + + worker.onmessage = function(e){ + var blob = e.data; + currCallback(blob); + } + + source.connect(this.node); + this.node.connect(this.context.destination); //this should not be necessary + }; + + Recorder.forceDownload = function(blob, filename){ + var url = (window.URL || window.webkitURL).createObjectURL(blob); + var link = window.document.createElement('a'); + link.href = url; + link.download = filename || 'output.wav'; + var click = document.createEvent("Event"); + click.initEvent("click", true, true); + link.dispatchEvent(click); + } + + window.Recorder = Recorder; + +})(window); \ No newline at end of file diff --git a/client/stylesheets/base.less b/client/stylesheets/base.less index 0b71eb0c1bd..32e8918d60e 100644 --- a/client/stylesheets/base.less +++ b/client/stylesheets/base.less @@ -75,6 +75,22 @@ blockquote { } } +.upload-preview { + background-color: #f5f5f5; + .upload-preview-file { + height: 200px; + background-size: contain; + background-repeat: no-repeat; + background-position: center center; + } +} + +.upload-preview-title { + background-color: #eee; + padding: 3px; + border-radius: 0 0 5px 5px; +} + .flex-center { display: -webkit-flex; display: flex; @@ -132,6 +148,10 @@ blockquote { display: none !important; } +.hidden { + display: none; +} + .small-title { font-size: 13px; text-transform: uppercase; @@ -641,8 +661,8 @@ label.required:after { cursor: pointer; margin-left: 7px; position: absolute; - top: 4px; - left: 5px; + top: 5px; + left: 0px; .transition(transform .2s ease-out .1s); i { display: block; @@ -1936,12 +1956,67 @@ a.github-fork { .message-form { > div { position: relative; + display: flex; + display: -webkit-flex; + .input-message-container { + width: 100%; + } + > .file { + width: 44px; + font-size: 22px; + padding: 6px 0; + text-align: center; + border: 1px solid #E7E7E7; + border-radius: 5px 0 0 5px; + border-right: none; + cursor: pointer; + color: #888; + background-color: #FCFCFC; + .transition(background-color 0.1s linear, color 0.1s linear); + &:hover { + background-color: #F1F1F1; + color: #666; + } + + input { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + width: 44px; + opacity: 0; + cursor: pointer; + } + input::-webkit-file-upload-button { + cursor: pointer; + } + } + > .mic, .stop-mic { + width: 40px; + font-size: 22px; + padding: 7px 0; + margin-left: 4px; + text-align: center; + cursor: pointer; + color: #888; + border-radius: 30px; + background-color: #FCFCFC; + .transition(background-color 0.1s linear, color 0.1s linear); + &:hover { + background-color: #F1F1F1; + color: #666; + } + } } textarea { display: block; padding-top: 9px; padding-bottom: 9px; padding-right: 38px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; overflow-y: hidden; resize: none; &.editing { @@ -1951,7 +2026,7 @@ a.github-fork { .icon-paper-plane { position: absolute; top: 2px; - right: 10px; + margin-left: -44px; cursor: pointer; padding: 8px; font-size: 16px; @@ -3310,12 +3385,18 @@ a.github-fork { } .side-nav { top: 0; - .transform(translateX(-100%)); + // .transform(translateX(-100%)); .transition(transform .3s ease-out); } .main-content { left: 0; .transition(transform .3s ease-out); + z-index: 1000; + background-color: white; + + &.notransition { + .transition(transform .0s); + } } .fixed-title h2 { margin-left: 45px; @@ -3424,3 +3505,10 @@ a.github-fork { border-width: 0 0 1px 0; } } + +.touch { + .footer { + padding-left: 10px; + padding-right: 10px; + } +} \ No newline at end of file diff --git a/client/views/app/home.coffee b/client/views/app/home.coffee index afe04111c4d..f3068684352 100644 --- a/client/views/app/home.coffee +++ b/client/views/app/home.coffee @@ -1,14 +1,5 @@ Template.home.helpers - arrowPosition: -> - return 'left' unless Session.equals('flexOpened', true) - flexOpened: -> - return 'opened' if Session.equals('flexOpened', true) - withParagraph: -> - return { - withParagraph: true - } - -Template.home.events - "click .flex-tab .more": (event) -> - Session.set('flexOpened', !Session.get('flexOpened')) - + title: -> + return RocketChat.settings.get 'Layout_Home_Title' + body: -> + return RocketChat.settings.get 'Layout_Home_Body' diff --git a/client/views/app/home.html b/client/views/app/home.html index 2bbfc80e50b..2983174eb66 100644 --- a/client/views/app/home.html +++ b/client/views/app/home.html @@ -3,54 +3,11 @@
{{> burger}}

- {{_ "Welcome_to_the"}} Rocket.Chat + {{title}}

- -
-

{{_ "Have_your_own_chat"}}

-

← {{_ "use_menu"}}.

-
- {{ > social withParagraph }} -
-

{{_ "Get_to_know_the_team" }}

- -
+ {{{body}}}
- diff --git a/client/views/app/message.coffee b/client/views/app/message.coffee index 63b4333bfe0..3e369c23dae 100644 --- a/client/views/app/message.coffee +++ b/client/views/app/message.coffee @@ -19,9 +19,9 @@ Template.message.helpers when 'r' then t('Room_name_changed', { room_name: this.msg, user_by: this.u.username }) when 'au' then t('User_added_by', { user_added: this.msg, user_by: this.u.username }) when 'ru' then t('User_removed_by', { user_removed: this.msg, user_by: this.u.username }) - when 'ul' then tr('User_left', { context: this.u.gender }, { user_left: this.u.username }) + when 'ul' then t('User_left', { user_left: this.u.username }) when 'nu' then t('User_added', { user_added: this.u.username }) - when 'uj' then tr('User_joined_channel', { context: this.u.gender }, { user: this.u.username }) + when 'uj' then t('User_joined_channel', { user: this.u.username }) when 'wm' then t('Welcome', { user: this.u.username }) when 'rtc' then RocketChat.callbacks.run 'renderRtcMessage', this else @@ -59,7 +59,7 @@ Template.message.onViewRendered = (context) -> ul = lastNode.parentElement wrapper = ul.parentElement - if context.urls?.length > 0 and Template.oembedBaseWidget? + if context.urls?.length > 0 and Template.oembedBaseWidget? and RocketChat.settings.get 'API_Embed' for item in context.urls do (item) -> urlNode = lastNode.querySelector('.body a[href="'+item.url+'"]') diff --git a/client/views/app/messagePopup.coffee b/client/views/app/messagePopup.coffee index be543f423b7..a9a12d6eb42 100644 --- a/client/views/app/messagePopup.coffee +++ b/client/views/app/messagePopup.coffee @@ -118,7 +118,7 @@ Template.messagePopup.onCreated -> firstPartValue = value.substr 0, caret lastPartValue = value.substr caret - firstPartValue = firstPartValue.replace(template.selectorRegex, template.prefix + this.getValue(template.value.curValue, template.data.collection) + template.suffix) + firstPartValue = firstPartValue.replace(template.selectorRegex, template.prefix + this.getValue(template.value.curValue, template.data.collection, firstPartValue) + template.suffix) template.input.value = firstPartValue + lastPartValue diff --git a/client/views/app/messagePopupConfig.coffee b/client/views/app/messagePopupConfig.coffee index bb0713c7488..d9663b1642b 100644 --- a/client/views/app/messagePopupConfig.coffee +++ b/client/views/app/messagePopupConfig.coffee @@ -21,11 +21,19 @@ Template.messagePopupConfig.helpers items.unshift all return items - getValue: (_id, collection) -> + getValue: (_id, collection, firstPartValue) -> if _id is '@all' - return 'all' + if firstPartValue.indexOf(' ') > -1 + return 'all' + + return 'all:' + + username = collection.findOne(_id)?.username + + if firstPartValue.indexOf(' ') > -1 + return username - return collection.findOne(_id)?.username + return username + ':' return config diff --git a/client/views/app/room.coffee b/client/views/app/room.coffee index c8d5b2a1b97..555e2edbec1 100644 --- a/client/views/app/room.coffee +++ b/client/views/app/room.coffee @@ -295,6 +295,9 @@ Template.room.helpers canJoin: -> return !! ChatRoom.findOne { _id: @_id, t: 'c' } + canRecordAudio: -> + return navigator.getUserMedia? or navigator.webkitGetUserMedia? + Template.room.events @@ -353,20 +356,15 @@ Template.room.events 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' - blob = item.getAsFile() - - newFile = new (FS.File)(blob) - newFile.name('Clipboard') - newFile.rid = Session.get('openedRoom') - newFile.recId = Random.id() - newFile.userId = Meteor.userId() - Files.insert newFile, (error, fileObj) -> - unless error - toastr.success 'Upload from clipboard succeeded!' + fileUpload files 'keydown .input-message': (event) -> Template.instance().chatMessages.keydown(@_id, event, Template.instance()) @@ -551,14 +549,43 @@ Template.room.events 'dropped .dropzone-overlay': (e) -> e.currentTarget.parentNode.classList.remove 'over' + files = [] FS?.Utility?.eachFile e, (file) -> - newFile = new (FS.File)(file) - newFile.rid = Session.get('openedRoom') - newFile.recId = Random.id() - newFile.userId = Meteor.userId() - Files.insert newFile, (error, fileObj) -> - unless error - toastr.success 'Upload succeeded!' + files.push + file: file + name: file.name + + fileUpload files + + '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 + filesToUpload.push + file: file + name: file.name + + fileUpload filesToUpload + + 'click .message-form .mic': (e, t) -> + AudioRecorder.start -> + t.$('.stop-mic').removeClass('hidden') + t.$('.mic').addClass('hidden') + + 'click .message-form .stop-mic': (e, t) -> + AudioRecorder.stop (blob) -> + fileUpload [{ + file: blob + type: 'audio' + name: 'Audio record' + }] + + t.$('.stop-mic').addClass('hidden') + t.$('.mic').removeClass('hidden') 'click .deactivate': -> username = Session.get('showUserInfo') diff --git a/client/views/app/room.html b/client/views/app/room.html index e21d2fb007b..ca1a46c1f72 100644 --- a/client/views/app/room.html +++ b/client/views/app/room.html @@ -45,10 +45,24 @@