Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarco Ambrosini <marcoambrosini@pm.me>2021-05-17 15:34:08 +0300
committerMarco Ambrosini <marcoambrosini@pm.me>2021-06-11 10:24:43 +0300
commitb7ec08402a6093db1d03d26cec7b2b105bfae2f1 (patch)
treebddd36eb3f6fb26f4f8cd28da125695cd61d37b0 /src/components/NewMessageForm
parentebff88cd28597b8933eb0e5970a6d4132fa5263e (diff)
Add ability to record audio files
Signed-off-by: Marco Ambrosini <marcoambrosini@pm.me>
Diffstat (limited to 'src/components/NewMessageForm')
-rw-r--r--src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue5
-rw-r--r--src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue277
-rw-r--r--src/components/NewMessageForm/NewMessageForm.vue37
3 files changed, 310 insertions, 9 deletions
diff --git a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
index e72d2febc..91df6c2f8 100644
--- a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
+++ b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
@@ -216,7 +216,7 @@ export default {
watch: {
text(text) {
this.$nextTick(() => {
- this.$emit('update:contentEditable', this.$refs.contentEditable.cloneNode(true))
+ this.$('update:contentEditable', this.$refs.contentEditable.cloneNode(true))
})
this.$emit('update:value', text)
@@ -235,6 +235,7 @@ export default {
}
},
},
+
mounted() {
this.focusInput()
/**
@@ -245,10 +246,12 @@ export default {
this.atWhoPanelExtraClasses = 'talk candidate-mentions'
},
+
beforeDestroy() {
EventBus.$off('routeChange', this.focusInput)
EventBus.$off('focusChatInput', this.focusInput)
},
+
methods: {
onBlur() {
// requires a short delay to avoid blocking click event handlers
diff --git a/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue b/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue
new file mode 100644
index 000000000..5a4119e95
--- /dev/null
+++ b/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue
@@ -0,0 +1,277 @@
+<!--
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ -
+ - @author Marco Ambrosini <marcoambrosini@pm.me>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<template>
+ <div
+ class="audio-recorder">
+ <button
+ v-if="!isRecording"
+ class="audio-recorder__trigger nc-button nc-button__main"
+ @click="start">
+ <Microphone
+ :size="16"
+ title=""
+ decorative />
+ </button>
+ <div v-else class="wrapper">
+ <button
+ class="audio-recorder__stop nc-button nc-button__main"
+ @click="abortRecording">
+ <Close
+ :size="16"
+ title=""
+ decorative />
+ </button>
+ <div class="audio-recorder__info">
+ <div class="recording-indicator fadeOutIn" />
+ <span
+ class="time">
+ {{ parsedRecordTime }}</span>
+ </div>
+ <button
+ class="audio-recorder__trigger nc-button nc-button__main"
+ :class="{'audio-recorder__trigger--recording': isRecording}"
+ @click="stop">
+ <Check
+ :size="16"
+ title=""
+ decorative />
+ </button>
+ </div>
+ </div>
+</template>
+
+<script>
+import Microphone from 'vue-material-design-icons/Microphone'
+import Close from 'vue-material-design-icons/Close'
+import Check from 'vue-material-design-icons/Check'
+
+import { mediaDevicesManager } from '../../../utils/webrtc/index'
+
+export default {
+ name: 'AudioRecorder',
+
+ components: {
+ Microphone,
+ Close,
+ Check,
+ },
+
+ data() {
+ return {
+ // The audio stream object
+ audioStream: null,
+ // The media recorder which generate the recorded chunks
+ mediaRecorder: null,
+ // The chunks array
+ chunks: [],
+ // The final audio file blob
+ blob: null,
+ // The blob url
+ URL: '',
+ // Switched to true if the recording is aborted
+ aborted: false,
+ // recordTimer
+ recordTimer: null,
+ // the record timer
+ recordTime: {
+ minutes: 0,
+ seconds: 0,
+ },
+ }
+ },
+
+ computed: {
+ // Recording state of the mediaRecorder
+ isRecording() {
+ if (this.mediaRecorder) {
+ return this.mediaRecorder.state === 'recording'
+ } else {
+ return false
+ }
+ },
+
+ parsedRecordTime() {
+ const seconds = this.recordTime.seconds.toString().length === 2 ? this.recordTime.seconds : `0${this.recordTime.seconds}`
+ const minutes = this.recordTime.minutes.toString().length === 2 ? this.recordTime.minutes : `0${this.recordTime.minutes}`
+ return `${minutes}:${seconds}`
+ },
+ },
+
+ watch: {
+
+ isRecording(newValue) {
+ console.debug('isRecording', newValue)
+ },
+ },
+
+ methods: {
+ /**
+ * Initialize the media stream and start capturing the audio
+ */
+ async start() {
+ try {
+ // Create new audio stream
+ this.audioStream = await mediaDevicesManager.getUserMedia({
+ audio: true,
+ })
+ // Create a mediarecorder to capture the stream
+ this.mediaRecorder = new MediaRecorder(this.audioStream)
+ // Add event handler to ondataAvailable
+ this.mediaRecorder.ondataavailable = (e) => {
+ this.chunks.push(e.data)
+ }
+ // Add event handler to onstop
+ this.mediaRecorder.onstop = this.generateFile
+ // Start the recording
+ this.mediaRecorder.start()
+ // Start the timer
+ this.recordTimer = setInterval(() => {
+ if (this.recordTime.seconds === 59) {
+ this.recordTime.minutes++
+ this.recordTime.seconds = 0
+ }
+ this.recordTime.seconds++
+ }, 1000)
+ // Forward an event to let the parent NewMessageForm component
+ // that there's an undergoing recording operation
+ this.$emit('recording', true)
+ console.debug(this.mediaRecorder.state)
+ } catch (exception) {
+ console.debug(exception)
+ }
+ },
+
+ /**
+ * Stop the mediaRecorder
+ */
+ stop() {
+ this.mediaRecorder.stop()
+ clearInterval(this.recordTimer)
+ this.$emit('recording', false)
+ },
+
+ /**
+ * Generate the file
+ */
+ generateFile() {
+ if (!this.aborted) {
+ this.blob = new Blob(this.chunks, { 'type': 'audio/mpeg-3' })
+ // Convert blob to file
+ const audioFile = new File([this.blob], `${Date.now()}.mp3`)
+ this.$emit('audioFile', audioFile)
+ this.$emit('recording', false)
+ this.URL = window.URL.createObjectURL(this.blob)
+ }
+ this.resetComponentData()
+ },
+
+ /**
+ * Aborts the recording operation.
+ */
+ abortRecording() {
+ this.aborted = true
+ this.stop()
+ },
+
+ /**
+ * Resets this component to its initial state
+ */
+ resetComponentData() {
+ this.audioStream = null
+ this.mediaRecorder = null
+ this.chunks = []
+ this.blob = null
+ this.aborted = false
+ this.isAudiorecorderActive = false
+ this.recordTime = {
+ minutes: 0,
+ seconds: 0,
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+
+@import '../../../assets/buttons';
+
+.audio-recorder {
+ display: flex;
+ // Audio record button
+ &__trigger {
+ &--recording {
+ background-color: var(--color-success) !important;
+ opacity: .8;
+ color: white;
+ &:hover,
+ &:focus {
+ opacity: 1;
+ }
+ }
+ }
+
+ &__stop {
+ margin-left: 4px;
+ background-color: var(--color-error) !important;
+ color: white;
+ opacity: .8;
+ &:hover,
+ &:focus {
+ opacity: 1;
+ }
+ }
+
+ &__info {
+ width: 86px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ .time {
+ flex: 0 0 50px;
+ }
+ .recording-indicator {
+ width: 16px;
+ height: 16px;
+ flex: 0 0 16px;
+ border-radius: 8px;
+ background-color: var(--color-error);
+ margin: 8px;
+ }
+ }
+}
+
+.wrapper {
+ display: flex;
+}
+
+@keyframes fadeOutIn {
+ 0% { opacity:1; }
+ 50% { opacity:.3; }
+ 100% { opacity:1; }
+}
+
+.fadeOutIn {
+ animation: fadeOutIn 3s infinite;
+}
+
+</style>
diff --git a/src/components/NewMessageForm/NewMessageForm.vue b/src/components/NewMessageForm/NewMessageForm.vue
index 3abc8f56d..0cf2b45ec 100644
--- a/src/components/NewMessageForm/NewMessageForm.vue
+++ b/src/components/NewMessageForm/NewMessageForm.vue
@@ -103,7 +103,14 @@
@submit="handleSubmit"
@files-pasted="handlePastedFiles" />
</div>
+
+ <AudioRecorder
+ v-if="!hasText"
+ @recording="handleRecording"
+ @audioFile="handleAudioFile" />
+
<button
+ v-if="hasText"
:disabled="disabled"
type="submit"
:aria-label="t('spreed', 'Send message')"
@@ -133,6 +140,7 @@ import { CONVERSATION } from '../../constants'
import Paperclip from 'vue-material-design-icons/Paperclip'
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline'
import Send from 'vue-material-design-icons/Send'
+import AudioRecorder from './AudioRecorder/AudioRecorder'
const picker = getFilePickerBuilder(t('spreed', 'File to share'))
.setMultiSelect(false)
@@ -152,6 +160,7 @@ export default {
EmojiPicker,
EmoticonOutline,
Send,
+ AudioRecorder,
},
props: {
@@ -160,13 +169,17 @@ export default {
required: true,
},
},
+
data: function() {
return {
text: '',
parsedText: '',
conversationIsFirstInList: false,
+ // True when the audiorecorder component is recording
+ isRecordingAudio: false,
}
},
+
computed: {
/**
* The current conversation token
@@ -188,7 +201,7 @@ export default {
},
disabled() {
- return this.isReadOnly || !this.currentConversationIsJoined
+ return this.isReadOnly || !this.currentConversationIsJoined || this.isRecordingAudio
},
placeholderText() {
@@ -227,6 +240,10 @@ export default {
currentConversationIsJoined() {
return this.$store.getters.currentConversationIsJoined
},
+
+ hasText() {
+ return this.text !== ''
+ },
},
watch: {
@@ -255,6 +272,7 @@ export default {
EventBus.$on('uploadStart', this.handleUploadStart)
EventBus.$on('retryMessage', this.handleRetryMessage)
this.text = this.$store.getters.currentMessageInput(this.token) || ''
+ // this.startRecording()
},
beforeDestroy() {
@@ -442,6 +460,14 @@ export default {
range.setStartAfter(emojiTextNode)
},
+
+ handleAudioFile(payload) {
+ this.handleFiles([payload])
+ },
+
+ handleRecording(payload) {
+ this.isRecordingAudio = payload
+ },
},
}
</script>
@@ -454,6 +480,7 @@ export default {
justify-content: center;
padding: 12px 0;
border-top: 1px solid var(--color-border);
+ height: 69px;
&--chatScrolledToBottom {
border-top: none;
}
@@ -467,7 +494,7 @@ export default {
display: flex;
position:relative;
flex: 0 1 650px;
- margin: 0 48px;
+ margin: 0 4px;
&__emoji-picker {
position: absolute;
left: 6px;
@@ -489,12 +516,6 @@ export default {
bottom: 0;
}
- &__send-button {
- position: absolute;
- right: -44px;
- bottom: 0;
- }
-
&__input {
flex-grow: 1;
max-height: $message-form-max-height;