From e9144e5703d392cd8c84cd34290deb8dc537c3ae Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Tue, 30 Jun 2015 19:52:16 +0200 Subject: [PATCH 1/6] voice recorder proposal for #190 --- app/js/controllers.js | 2 + app/js/directives.js | 66 +++++++++++++++++++++++++ app/js/init.js | 3 +- app/js/locales/en-us.json | 1 + app/less/mobile.less | 99 +++++++++++++++++++++++++++++++++++-- app/manifest.webapp | 3 ++ app/partials/mobile/im.html | 17 ++++++- 7 files changed, 184 insertions(+), 7 deletions(-) diff --git a/app/js/controllers.js b/app/js/controllers.js index 65fd936d..c1cea6bd 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -473,6 +473,8 @@ angular.module('myApp.controllers', ['myApp.i18n']) skipped: false }; + $scope.voiceRecorder = { time : '', recording : null }; + $scope.openSettings = function () { $modal.open({ templateUrl: templateUrl('settings_modal'), diff --git a/app/js/directives.js b/app/js/directives.js index 114f7d95..70ffd24c 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1449,9 +1449,12 @@ angular.module('myApp.directives', ['myApp.filters']) var messageFieldWrap = $('.im_send_field_wrap', element)[0]; var dragStarted, dragTimeout; var submitBtn = $('.im_submit', element)[0]; + var voiceRecord = $('.im_record', element); var stickerImageCompiled = $compile(''); var cachedStickerImages = {}; + var audioRecorder = null; + var audioStream = null; var emojiTooltip = new EmojiTooltip(emojiButton, { getStickers: function (callback) { @@ -1552,6 +1555,69 @@ angular.module('myApp.directives', ['myApp.filters']) }); }); + if (Config.Navigator.ffos) { + + navigator.getUserMedia = ( navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); + + voiceRecord.on('touchstart', function(e) { + navigator.getUserMedia({audio : true}, function(stream){ + var start = Date.now(); + var touch = null; + + audioStream = stream; + audioRecorder = new MediaRecorder(stream); + + var interval = setInterval(function(){ + var time = (new Date()); + + time.setTime(Date.now() - start); + + $scope.$apply(function(){ + $scope.$parent.$parent.voiceRecorder.time = (time.getMinutes() < 10 ? '0' : '') + time.getMinutes() + ':' + (time.getSeconds() < 10 ? '0' : '') + time.getSeconds(); + }); + }, 1000); + + $scope.$apply(function(){ + $scope.$parent.$parent.voiceRecorder.time = '00:00'; + $scope.$parent.$parent.voiceRecorder.recording = interval; + }); + + audioRecorder.start(); + + console.log('recording now!'); + + }, function(e){ + console.error(e); + }); + }); + + voiceRecord.on('click', function(){ + if (audioRecorder) { + audioRecorder.ondataavailable = function(e){ + var blob = e.data; + + console.log(blob); + $scope.draftMessage.files = [blob]; + $scope.draftMessage.isMedia = true; + } + } + }); + + voiceRecord.on('touchend', function(){ + if (audioRecorder) { + audioRecorder.stop(); + audioStream.stop(); + + clearInterval($scope.$parent.$parent.voiceRecorder.recording); + + $scope.$apply(function(){ + $scope.$parent.$parent.voiceRecorder.recording = null; + }); + } + }); + + } + var sendOnEnter = true; function updateSendSettings () { Storage.get('send_ctrlenter').then(function (sendOnCtrl) { diff --git a/app/js/init.js b/app/js/init.js index 4cc3b4be..8cd74586 100644 --- a/app/js/init.js +++ b/app/js/init.js @@ -65,7 +65,8 @@ (function initApplication () { var classes = [ Config.Navigator.osX ? 'osx' : 'non_osx', - Config.Navigator.retina ? 'is_2x' : 'is_1x' + Config.Navigator.retina ? 'is_2x' : 'is_1x', + Config.Navigator.ffos ? 'ffos' : 'non_ffos' ]; if (Config.Modes.ios_standalone) { classes.push('ios_standalone'); diff --git a/app/js/locales/en-us.json b/app/js/locales/en-us.json index eca49eb8..208a72d5 100644 --- a/app/js/locales/en-us.json +++ b/app/js/locales/en-us.json @@ -470,6 +470,7 @@ "im_attach_file_title": "Send file", "im_emoji_btn_title": "Insert emoticon", "im_submit_message": "Send", + "im_voice_recorder_label": "Swipe left to abort", "login_sign_in": "Sign in", "login_enter_number_description": "Please choose your country and enter your full phone number.", diff --git a/app/less/mobile.less b/app/less/mobile.less index b554febb..9b1b6f10 100644 --- a/app/less/mobile.less +++ b/app/less/mobile.less @@ -1368,7 +1368,7 @@ a.im_message_fwd_author { } } -.icon-paperclip { +.icon-paperclip, .icon-mic { display: inline-block; width: 19px; height: 23px; @@ -1379,12 +1379,16 @@ a.im_message_fwd_author { background-position: -12px -68px; } -.im_attach { +.icon-mic { + background-position: -12px -285px; +} + +.im_attach, .im_record { cursor: pointer; display: none; overflow: hidden; position: absolute; - right: 0; + right: 34px; top: 0; margin: 0; width: 50px; @@ -1399,6 +1403,32 @@ a.im_message_fwd_author { } } +.non_ffos { + .im_attach { + right: 0; + } +} + +.im_record { + right: 0; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.ffos { + .im_send_form_empty { + .im_send_field_wrap { + margin-right: 85px; + } + + .im_record { + display: block; + } + } +} + .im_send_form_empty { .im_submit { display: none; @@ -1409,6 +1439,28 @@ a.im_message_fwd_author { } } +.im_voice_recording { + background-color: rgb(23, 23, 23); + color: white; + + .im_voice_recorder_wrap { + display: block; + } + + .im_send_field_wrap { + display: none; + } + + .im_attach { + display: none; + } + + .composer_emoji_insert_btn { + display: none; + } + +} + .icon-emoji { display: inline-block; width: 22px; @@ -1449,6 +1501,45 @@ a.im_message_fwd_author { } } +.im_voice_recorder_wrap { + margin-left: 0px; + padding-left: 10px; + height: 38px; + display: none; + line-height: 38px; + color: white; +} + +.im_recorder_indicator, .im_recorder_time { + float: left; + vertical-align: middle; +} + +.im_recorder_indicator i { + background-color: #F00; + height: 10px; + width: 10px; + border-radius: 50%; + margin-right: 5px; + vertical-align: middle; + display: inline-block; +} + +.im_recorder_label { + overflow: auto; + font-size: 17px; + text-align: center; + margin-right: 50px; + + i, span { + vertical-align: middle; + } + + i { + margin-right: 5px; + } +} + .composer_rich_textarea { min-height: 18px; max-height: 136px; @@ -1810,4 +1901,4 @@ a.media_modal_date:hover { } .im_send_keyboard_wrap { padding: 0 5px; -} \ No newline at end of file +} diff --git a/app/manifest.webapp b/app/manifest.webapp index 697a9f82..333ce733 100644 --- a/app/manifest.webapp +++ b/app/manifest.webapp @@ -44,6 +44,9 @@ "device-storage:videos": { "description": "Required for videos download", "access": "createonly" + }, + "audio-capture" : { + "description" : "Required to record voice messages" } }, "activities": { diff --git a/app/partials/mobile/im.html b/app/partials/mobile/im.html index e15bf62b..8d01c015 100644 --- a/app/partials/mobile/im.html +++ b/app/partials/mobile/im.html @@ -130,7 +130,7 @@ -
+
@@ -156,11 +156,24 @@
+
+
+
{{voiceRecorder.time}}
+
+ + +
+
+
+
+ +
+ @@ -187,4 +200,4 @@
-
\ No newline at end of file + From 57fa8cf6fbd583f0e8236ae3c83157504cb289fa Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Fri, 10 Jul 2015 13:45:06 +0200 Subject: [PATCH 2/6] slight UX improvement --- app/js/controllers.js | 2 +- app/js/directives.js | 7 ++++++- app/less/mobile.less | 12 +++++++++++- app/partials/mobile/im.html | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/js/controllers.js b/app/js/controllers.js index c1cea6bd..4c30f366 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -473,7 +473,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) skipped: false }; - $scope.voiceRecorder = { time : '', recording : null }; + $scope.voiceRecorder = { time : '', recording : null, processing : false }; $scope.openSettings = function () { $modal.open({ diff --git a/app/js/directives.js b/app/js/directives.js index 70ffd24c..bf9f4837 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1593,17 +1593,22 @@ angular.module('myApp.directives', ['myApp.filters']) voiceRecord.on('click', function(){ if (audioRecorder) { + $scope.$parent.$parent.voiceRecorder.processing = true; + audioRecorder.ondataavailable = function(e){ var blob = e.data; console.log(blob); $scope.draftMessage.files = [blob]; $scope.draftMessage.isMedia = true; + + audioRecorder = null; + $scope.$parent.$parent.voiceRecorder.processing = false; } } }); - voiceRecord.on('touchend', function(){ + $($window).on('touchend', function(){ if (audioRecorder) { audioRecorder.stop(); audioStream.stop(); diff --git a/app/less/mobile.less b/app/less/mobile.less index 9b1b6f10..513d87de 100644 --- a/app/less/mobile.less +++ b/app/less/mobile.less @@ -1439,7 +1439,7 @@ a.im_message_fwd_author { } } -.im_voice_recording { +.im_voice_recording, .im_processing_recording { background-color: rgb(23, 23, 23); color: white; @@ -1461,6 +1461,16 @@ a.im_message_fwd_author { } +.im_processing_recording { + .im_recorder_indicator i { + background-color: green; + } + + .im_record { + display: none; + } +} + .icon-emoji { display: inline-block; width: 22px; diff --git a/app/partials/mobile/im.html b/app/partials/mobile/im.html index 8d01c015..78b6bcba 100644 --- a/app/partials/mobile/im.html +++ b/app/partials/mobile/im.html @@ -130,7 +130,7 @@ -
+
From e1cc8b88295c2dbe2a497f2b72b00ca345f77853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lerique?= Date: Tue, 15 Sep 2015 15:24:09 +0200 Subject: [PATCH 3/6] Use a promise to gather the recorded data `click` is when we know if we should use the recorded data or discard it, but `click` fires after `touchend`. So to avoid stopping the recording and registering `ondataavailable` afterwards, register a promise on `ondataavailable`, and gather the data if needed in `click`. --- app/js/directives.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/js/directives.js b/app/js/directives.js index bf9f4837..bbde5aad 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1454,6 +1454,7 @@ angular.module('myApp.directives', ['myApp.filters']) var stickerImageCompiled = $compile(''); var cachedStickerImages = {}; var audioRecorder = null; + var audioPromise = null; var audioStream = null; var emojiTooltip = new EmojiTooltip(emojiButton, { @@ -1595,7 +1596,7 @@ angular.module('myApp.directives', ['myApp.filters']) if (audioRecorder) { $scope.$parent.$parent.voiceRecorder.processing = true; - audioRecorder.ondataavailable = function(e){ + audioPromise.then(function(e) { var blob = e.data; console.log(blob); @@ -1604,12 +1605,16 @@ angular.module('myApp.directives', ['myApp.filters']) audioRecorder = null; $scope.$parent.$parent.voiceRecorder.processing = false; - } + }); } }); $($window).on('touchend', function(){ if (audioRecorder) { + audioPromise = new Promise(function(resolve) { + audioRecorder.ondataavailable = resolve; + }); + audioRecorder.stop(); audioStream.stop(); From ad66b39ff8803e41956aa1c91933b55f71efc00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lerique?= Date: Tue, 15 Sep 2015 15:26:46 +0200 Subject: [PATCH 4/6] Prevent a new recording from starting while processing --- app/js/directives.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/js/directives.js b/app/js/directives.js index bbde5aad..90cdd715 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1561,6 +1561,7 @@ angular.module('myApp.directives', ['myApp.filters']) navigator.getUserMedia = ( navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); voiceRecord.on('touchstart', function(e) { + if ($scope.$parent.$parent.voiceRecorder.processing) { return; } navigator.getUserMedia({audio : true}, function(stream){ var start = Date.now(); var touch = null; From 49029b301be0c7a512bc418c086939e5d6036f2d Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Fri, 13 Nov 2015 08:41:51 +0100 Subject: [PATCH 5/6] don't access dead objects Some clean up to prevent the app from accessing the dead stream object. --- app/js/directives.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/js/directives.js b/app/js/directives.js index 90cdd715..b86e3b62 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1562,10 +1562,12 @@ angular.module('myApp.directives', ['myApp.filters']) voiceRecord.on('touchstart', function(e) { if ($scope.$parent.$parent.voiceRecorder.processing) { return; } + navigator.getUserMedia({audio : true}, function(stream){ var start = Date.now(); var touch = null; + audioPromise = null; audioStream = stream; audioRecorder = new MediaRecorder(stream); @@ -1594,7 +1596,7 @@ angular.module('myApp.directives', ['myApp.filters']) }); voiceRecord.on('click', function(){ - if (audioRecorder) { + if (audioPromise) { $scope.$parent.$parent.voiceRecorder.processing = true; audioPromise.then(function(e) { @@ -1604,14 +1606,15 @@ angular.module('myApp.directives', ['myApp.filters']) $scope.draftMessage.files = [blob]; $scope.draftMessage.isMedia = true; - audioRecorder = null; $scope.$parent.$parent.voiceRecorder.processing = false; + + audioPromise = null; }); } }); $($window).on('touchend', function(){ - if (audioRecorder) { + if (audioStream && audioRecorder) { audioPromise = new Promise(function(resolve) { audioRecorder.ondataavailable = resolve; }); @@ -1619,6 +1622,9 @@ angular.module('myApp.directives', ['myApp.filters']) audioRecorder.stop(); audioStream.stop(); + audioRecorder = null; + audioStream = null; + clearInterval($scope.$parent.$parent.voiceRecorder.recording); $scope.$apply(function(){ From 20b20ba613f05bf82ef2ea2c232fc1aa364dd6bb Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Fri, 13 Nov 2015 09:12:08 +0100 Subject: [PATCH 6/6] fix for a wrong merge conflic resolve --- app/js/directives.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/js/directives.js b/app/js/directives.js index b86e3b62..4630ac58 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1429,7 +1429,8 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('mySendForm', function (_, $timeout, $compile, $modalStack, $http, $interpolate, Storage, AppStickersManager, AppDocsManager, ErrorService, shouldFocusOnInteraction) { + .directive('mySendForm', function (_, $window, $compile, $modalStack, $http, $interpolate, Storage, AppStickersManager, AppDocsManager, ErrorService, shouldFocusOnInteraction) { + return { link: link, scope: {