Message: Allow sent recorded audio as attachment

pull/4006/head
Angel Fernando Quiroz Campos 4 years ago
parent 714fb3c9af
commit e543f00657
  1. 60
      assets/vue/components/AudioRecorder.vue
  2. 26
      assets/vue/components/message/Form.vue
  3. 8
      assets/vue/main.js
  4. 3
      assets/vue/services/messageattachment.js
  5. 11
      assets/vue/views/message/Create.vue
  6. 51
      src/CoreBundle/Controller/Api/CreateMessageAttachmentAction.php
  7. 2
      src/CoreBundle/Entity/Message.php
  8. 37
      src/CoreBundle/Entity/MessageAttachment.php

@ -1,26 +1,37 @@
<template> <template>
<div class="py-2"> <div class="py-4">
<q-btn v-if="!isRecording" color="primary" icon="mic" :label="$t('Start recording')" @click="record()"/> <q-btn v-if="!isRecording" color="primary" icon="mic" :label="$t('Start recording')" @click="record()"/>
<q-btn v-if="isRecording" color="red" icon="stop" :label="$t('Stop recording')" @click="stop()"/> <q-btn v-if="isRecording" :label="$t('Stop recording')" color="red" icon="stop" @click="stop()"/>
<div v-for="(audio, index) in audioList" :key="index" class="py-2"> <div v-for="(audio, index) in audioList" :key="index" class="py-2">
<audio controls style="max-width: 100%;"> <audio controls class="max-w-full">
<source :src="audio"> <source :src="URL.createObjectURL(audio)">
</audio> </audio>
{{ $t('Size') }} {{ audio.size }}
<q-btn :label="$t('Attach')" class="my-1" color="green" icon="attachment" size="sm"
@click="attachAudio(audio)"/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {ref} from "vue"; import {reactive, toRefs} from "vue";
export default { export default {
name: "AudioRecorder", name: "AudioRecorder",
props: {
setup() { multiple: {
const isRecording = ref(false) type: Boolean,
const audioList = ref([]); required: false,
default: true
},
},
setup(props, {emit}) {
const recorderState = reactive({
isRecording: false,
audioList: [],
});
let mediaRecorder = null; let mediaRecorder = null;
let mediaChunks = []; let mediaChunks = [];
@ -37,16 +48,17 @@ export default {
mediaChunks.push(e.data) mediaChunks.push(e.data)
}; };
mediaRecorder.onstop = e => { mediaRecorder.onstop = e => {
const blob = new Blob(mediaChunks, {type: 'audio/ogg; codecs=opus'}); stream.getAudioTracks()[0].stop();
const audioUrl = URL.createObjectURL(blob);
const audioItem = new Blob(mediaChunks, {type: 'audio/ogg; codecs=opus'});
mediaChunks = []; mediaChunks = [];
audioList.value.push(audioUrl); recorderState.audioList.push(audioItem);
}; };
mediaRecorder.start(); mediaRecorder.start();
isRecording.value = true; recorderState.isRecording = true;
}) })
.catch(console.log); .catch(console.log);
} }
@ -56,16 +68,30 @@ export default {
return; return;
} }
if (false === props.multiple && recorderState.audioList.length > 0) {
recorderState.audioList.shift();
}
mediaRecorder.stop(); mediaRecorder.stop();
mediaRecorder.stream.getAudioTracks()[0].stop(); recorderState.isRecording = false;
isRecording.value = false; }
function attachAudio(audio) {
emit('attach-audio', audio);
const index = recorderState.audioList.indexOf(audio);
if (index >= 0) {
recorderState.audioList.splice(index, 1);
}
} }
return { return {
record, record,
stop, stop,
audioList, ...toRefs(recorderState),
isRecording attachAudio,
URL
}; };
} }
} }

@ -15,9 +15,20 @@
<slot></slot> <slot></slot>
</v-col> </v-col>
<v-col md="3"> <v-col md="3">
<div v-text="$t('Atachments')" class="text-h6"/> <div v-if="item.attachments && item.attachments.length > 0">
<div v-text="$t('Atachments')" class="text-h6"/>
<ul>
<li v-for="(attachment, index) in item.attachments" :key="index" class="my-2">
<audio v-if="attachment.type.indexOf('audio') === 0" controls class="max-w-full">
<source :src="URL.createObjectURL(attachment)">
</audio>
</li>
</ul>
<AudioRecorder></AudioRecorder> <hr class="my-2">
</div>
<AudioRecorder @attach-audio="attachAudios"></AudioRecorder>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@ -34,7 +45,7 @@ export default {
name: 'MessageForm', name: 'MessageForm',
components: {AudioRecorder}, components: {AudioRecorder},
setup() { setup() {
return { v$: useVuelidate() } return { v$: useVuelidate(), URL }
}, },
props: { props: {
values: { values: {
@ -89,6 +100,15 @@ export default {
return this.errors || {}; return this.errors || {};
} }
}, },
methods: {
attachAudios(audio) {
if (!this.item.attachments) {
this.item.attachments = [];
}
this.item.attachments.push(audio);
}
},
validations: { validations: {
item: { item: {
title: { title: {

@ -14,6 +14,7 @@ import personalFileService from './services/personalfile';
import resourceLinkService from './services/resourcelink'; import resourceLinkService from './services/resourcelink';
import resourceNodeService from './services/resourcenode'; import resourceNodeService from './services/resourcenode';
import messageService from './services/message'; import messageService from './services/message';
import messageAttachmentService from './services/messageattachment';
import messageRelUserService from './services/messagereluser'; import messageRelUserService from './services/messagereluser';
import userService from './services/user'; import userService from './services/user';
import userGroupService from './services/usergroup'; import userGroupService from './services/usergroup';
@ -95,6 +96,13 @@ store.registerModule(
}) })
); );
store.registerModule(
'messageattachment',
makeCrudModule({
service: messageAttachmentService
})
);
store.registerModule( store.registerModule(
'messagereluser', 'messagereluser',
makeCrudModule({ makeCrudModule({

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('message_attachments');

@ -175,6 +175,17 @@ export default {
}), }),
}, },
methods: { methods: {
onCreated(message) {
if (this.item.attachments) {
this.item.attachments.forEach(attachment => {
this.createWithFormData({
messageId: message.id,
file: attachment
});
})
}
},
...mapActions('messageattachment', ['createWithFormData']),
...mapActions('message', ['create', 'reset']) ...mapActions('message', ['create', 'reset'])
} }
}; };

@ -0,0 +1,51 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\MessageAttachment;
use Chamilo\CoreBundle\Repository\Node\MessageAttachmentRepository;
use Doctrine\ORM\EntityManager;
use Exception;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class CreateMessageAttachmentAction extends BaseResourceFileAction
{
/**
* @throws Exception
*/
public function __invoke(Request $request, MessageAttachmentRepository $repo, EntityManager $em): MessageAttachment
{
/** @var UploadedFile $uploadedFile */
$uploadedFile = $request->files->get('file');
if (!$uploadedFile) {
throw new BadRequestHttpException('file is required');
}
$messageRepo = $em->getRepository(Message::class);
$message = $messageRepo->find($request->get('messageId'));
$attachment = (new MessageAttachment())
->setFilename($uploadedFile->getFilename())
->setMessage($message)
->setParent($message->getSender())
->setCreator($message->getSender())
;
$message->addAttachment($attachment);
$em->persist($attachment);
$repo->addFile($attachment, $uploadedFile);
$em->flush();
return $attachment;
}
}

@ -234,7 +234,7 @@ class Message
/** /**
* @var Collection|MessageAttachment[] * @var Collection|MessageAttachment[]
* *
* @ORM\OneToMany(targetEntity="MessageAttachment", mappedBy="message", cascade={"remove"}) * @ORM\OneToMany(targetEntity="MessageAttachment", mappedBy="message", cascade={"remove", "persist"})
*/ */
protected Collection $attachments; protected Collection $attachments;

@ -6,6 +6,8 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity; namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Chamilo\CoreBundle\Controller\Api\CreateMessageAttachmentAction;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** /**
@ -14,6 +16,39 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Table(name="message_attachment") * @ORM\Table(name="message_attachment")
* @ORM\Entity(repositoryClass="Chamilo\CoreBundle\Repository\Node\MessageAttachmentRepository") * @ORM\Entity(repositoryClass="Chamilo\CoreBundle\Repository\Node\MessageAttachmentRepository")
*/ */
#[ApiResource(
collectionOperations: [
'get',
'post' => [
'controller' => CreateMessageAttachmentAction::class,
'deserialize' => false,
'validation_groups' => ['Default', 'message_attachment:create'],
'openapi_context' => [
'requestBody' => [
'content' => [
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'properties' => [
'file' => [
'type' => 'string',
'format' => 'binary',
],
'messageId' => [
'type' => 'integer',
],
],
],
],
],
],
],
],
],
iri: 'http://schema.org/MediaObject',
itemOperations: ['get'],
normalizationContext: ['groups' => 'message:read']
)]
class MessageAttachment extends AbstractResource implements ResourceInterface class MessageAttachment extends AbstractResource implements ResourceInterface
{ {
/** /**
@ -39,7 +74,7 @@ class MessageAttachment extends AbstractResource implements ResourceInterface
protected int $size; protected int $size;
/** /**
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Message", inversedBy="attachments") * @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Message", inversedBy="attachments", cascade={"persist"})
* @ORM\JoinColumn(name="message_id", referencedColumnName="id", nullable=false) * @ORM\JoinColumn(name="message_id", referencedColumnName="id", nullable=false)
*/ */
protected Message $message; protected Message $message;

Loading…
Cancel
Save