From 96b719017b7a32a81729a8f09dd79a79171bf175 Mon Sep 17 00:00:00 2001 From: giquieu Date: Fri, 4 Mar 2022 11:13:07 -0300 Subject: [PATCH] feat: Ability to send voice message to channel (#4064) Created the possibility to send audio as an attachment via the dashboard. The channels that can send audio are the same channels that can send any type of attachment. Used RecordRTC (https://github.com/muaz-khan/RecordRTC) to capture the audio and Wavesurfer (https://github.com/katspaugh/wavesurfer.js) to display the audio waves. RecordRTC can be used to record videos if necessary. Fixes #1973 --- .../components/widgets/AttachmentsPreview.vue | 5 + .../widgets/WootWriter/AudioRecorder.vue | 223 ++++++++++++++++++ .../widgets/WootWriter/ReplyBottomPanel.vue | 65 ++++- .../widgets/conversation/ReplyBox.vue | 67 +++++- .../i18n/locale/en/conversation.json | 6 + .../FluentIcon/dashboard-icons.json | 5 + app/javascript/shared/helpers/FileHelper.js | 2 +- package.json | 4 +- 8 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue diff --git a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue index 250c5c3c6..3d5846215 100644 --- a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue +++ b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue @@ -25,6 +25,7 @@
+
+
+
+ + + + + diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index 6df1b2858..6dcb47532 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -11,7 +11,6 @@ size="small" @click="toggleEmojiPicker" /> - + + + {{ recordingAudioDurationText }} + ({}), @@ -134,6 +158,10 @@ export default { type: Boolean, default: false, }, + showAudioRecorder: { + type: Boolean, + default: false, + }, onFileUpload: { type: Function, default: () => {}, @@ -146,6 +174,22 @@ export default { type: Function, default: () => {}, }, + toggleAudioRecorder: { + type: Function, + default: () => {}, + }, + toggleAudioRecorderPlayPause: { + type: Function, + default: () => {}, + }, + isRecordingAudio: { + type: Boolean, + default: false, + }, + recordingAudioState: { + type: String, + default: '', + }, isSendDisabled: { type: Boolean, default: false, @@ -192,9 +236,28 @@ export default { showAttachButton() { return this.showFileUpload || this.isNote; }, + showAudioRecorderButton() { + return this.showAudioRecorder; + }, + showAudioPlayStopButton() { + return this.showAudioRecorder && this.isRecordingAudio; + }, allowedFileTypes() { return ALLOWED_FILE_TYPES; }, + audioRecorderPlayStopIcon() { + switch (this.recordingAudioState) { + // playing paused recording stopped inactive destroyed + case 'playing': + return 'microphone-pause'; + case 'paused': + return 'microphone-play'; + case 'stopped': + return 'microphone-play'; + default: + return 'microphone-stop'; + } + }, showMessageSignatureButton() { return !this.isPrivate && this.isAnEmailChannel; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 37fbb4367..5a9253fba 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -33,8 +33,15 @@ :cc-emails.sync="ccEmails" :bcc-emails.sync="bccEmails" /> + this.$refs.messageInput.focus()); @@ -535,10 +568,26 @@ export default { this.attachedFiles = []; this.ccEmails = ''; this.bccEmails = ''; + this.isRecordingAudio = false; }, toggleEmojiPicker() { this.showEmojiPicker = !this.showEmojiPicker; }, + toggleAudioRecorder() { + this.isRecordingAudio = !this.isRecordingAudio; + this.isRecorderAudioStopped = !this.isRecordingAudio; + if (!this.isRecordingAudio) { + this.clearMessage(); + } + }, + toggleAudioRecorderPlayPause() { + if (this.isRecordingAudio && !this.isRecorderAudioStopped) { + this.isRecorderAudioStopped = true; + this.$refs.audioRecorderInput.stopAudioRecording(); + } else if (this.isRecordingAudio && this.isRecorderAudioStopped) { + this.$refs.audioRecorderInput.playPause(); + } + }, hideEmojiPicker() { if (this.showEmojiPicker) { this.toggleEmojiPicker(); @@ -559,6 +608,20 @@ export default { onFocus() { this.isFocused = true; }, + onStateRecorderTimerChanged(time) { + this.recordingAudioDuration = time; + }, + onStateRecorderChanged(state) { + this.recordingAudioState = state; + if (state.includes('notallowederror')) { + this.toggleAudioRecorder(); + } + }, + onRecorderBlob(file) { + if (file) { + this.onFileUpload(file); + } + }, toggleTyping(status) { const conversationId = this.currentChat.id; const isPrivate = this.isPrivate; diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 96ba4b429..ac25a5aed 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -74,8 +74,14 @@ "TIP_FORMAT_ICON": "Show rich text editor", "TIP_EMOJI_ICON": "Show emoji selector", "TIP_ATTACH_ICON": "Attach files", + "TIP_AUDIORECORDER_ICON": "Record audio", + "TIP_AUDIORECORDER_PERMISSION": "Allow access to audio", + "TIP_AUDIORECORDER_ERROR": "Could not open the audio", "ENTER_TO_SEND": "Enter to send", "DRAG_DROP": "Drag and drop here to attach", + "START_AUDIO_RECORDING": "Start audio recording", + "STOP_AUDIO_RECORDING": "Stop audio recording", + "": "", "EMAIL_HEAD": { "ADD_BCC": "Add bcc", "CC": { diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 39064412c..0cbc85d22 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -85,6 +85,11 @@ "merge-outline": "M3 6.75A.75.75 0 0 1 3.75 6h4.5a.75.75 0 0 1 .53.22L13.56 11h5.878L15.72 7.28a.75.75 0 1 1 1.06-1.06l4.998 5a.75.75 0 0 1 0 1.06l-4.998 5a.75.75 0 1 1-1.06-1.06l3.718-3.72H13.56l-4.78 4.78a.75.75 0 0 1-.531.22h-4.5a.75.75 0 0 1 0-1.5h4.19l4.25-4.25L7.94 7.5H3.75A.75.75 0 0 1 3 6.75Z", "more-horizontal-outline": "M7.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM13.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM18 13.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z", "more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z", + "microphone-outline": "M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z", + "microphone-off-outline": "M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z", + "microphone-stop-outline": "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9", + "microphone-pause-outline": "M13,16V8H15V16H13M9,16V8H11V16H9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", + "microphone-play-outline": "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z", "number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z", "open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z", "people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z", diff --git a/app/javascript/shared/helpers/FileHelper.js b/app/javascript/shared/helpers/FileHelper.js index a784b2ba2..d9ca9f943 100644 --- a/app/javascript/shared/helpers/FileHelper.js +++ b/app/javascript/shared/helpers/FileHelper.js @@ -19,7 +19,7 @@ export const fileSizeInMegaBytes = bytes => { }; export const checkFileSizeLimit = (file, maximumUploadLimit) => { - const fileSize = file?.file?.size; + const fileSize = file?.file?.size || file?.size; const fileSizeInMB = fileSizeInMegaBytes(fileSize); return fileSizeInMB <= maximumUploadLimit; }; diff --git a/package.json b/package.json index 1703f63e0..4f794fa6d 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "prosemirror-state": "1.3.4", "prosemirror-view": "1.18.4", "query-string": "5", + "recordrtc": "^5.6.2", "semver": "7.3.5", "spinkit": "~1.2.5", "tailwindcss": "^1.9.6", @@ -71,7 +72,8 @@ "vuedraggable": "^2.24.3", "vuelidate": "0.7.6", "vuex": "~2.1.1", - "vuex-router-sync": "~4.1.2" + "vuex-router-sync": "~4.1.2", + "wavesurfer.js": "^5.2.0" }, "devDependencies": { "@babel/core": "7.13.16",