diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 3a5582f8b19cd85a7643683ef9addf9be6ca0078..6972d8f271722456d037251bf4964d47ec32a283 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1,3 +1,26 @@ +/* CHAT MESSAGES */ + +.chat-message { + margin-top: 5px; + margin-bottom: 5px; + font-size: .8rem; + width: 66%; +} +.chat-message.chat-message-own { + margin-left: auto; +} +.chat-message-content { + width: 100%; +} +.chat-message-body { + background-color: lightgrey; + padding: 8px; + border-radius: 3px; +} +.chat-message-own .chat-message-body { + background-color: #05728F; +} + /* AJAX LOADING */ #overlay { position: fixed; @@ -302,4 +325,13 @@ td:hover .fa.text-light-green, p:hover .fa.text-light-green, h4:hover .fa.text-l #history { max-height: 300px; overflow-y: auto; +} + +.btn-link { + border: 0; + padding: 0; + margin: 0; + display: inline; + vertical-align: baseline; + color: inherit; } \ No newline at end of file diff --git a/src/Controller/ChatMessageController.php b/src/Controller/ChatMessageController.php new file mode 100644 index 0000000000000000000000000000000000000000..ce9a794179af2d76580c4f2a51c3fbc1647e7d44 --- /dev/null +++ b/src/Controller/ChatMessageController.php @@ -0,0 +1,96 @@ +<?php + +namespace App\Controller; + +use App\Entity\ChatMessage; +use App\Entity\Entry; +use App\Entity\Lexicon; +use App\Form\ChatMessageType; +use App\Repository\ChatMessageRepository; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @Route("/chat") + */ +class ChatMessageController extends AppBaseController +{ + /** + * @Route("/new", name="app_chat_message_new", methods={"GET", "POST"}) + */ + public function new(Request $request, ChatMessageRepository $chatMessageRepository): Response + { + $chatMessage = new ChatMessage(); + $chatMessage->setCreatedBy($this->getUser()); + if ($entryId = $request->get('entryId')) { + $entry = $this->em->getRepository(Entry::class)->find($entryId); + $chatMessage->setEntry($entry); + } + if ($lexiconId = $request->get('lexiconId')) { + $lexicon = $this->em->getRepository(Lexicon::class)->find($lexiconId); + $chatMessage->setLexicon($lexicon); + } + + $form = $this->createForm(ChatMessageType::class, $chatMessage, [ + 'action' => $this->generateUrl('app_chat_message_new', [ + 'entryId' => $entryId, + 'lexiconId' => $lexiconId, + ]) + ]); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $chatMessageRepository->add($chatMessage, true); + + return $this->render('closeModalAndReload.html.twig'); + } + + return $this->render('genericModalForm.html.twig', [ + 'title' => "Veuillez saisir votre message", + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}/edit", name="app_chat_message_edit", methods={"GET", "POST"}) + */ + public function edit(Request $request, ChatMessage $chatMessage, ChatMessageRepository $chatMessageRepository): Response + { + $form = $this->createForm(ChatMessageType::class, $chatMessage, [ + 'action' => $this->generateUrl('app_chat_message_edit', ['id' => $chatMessage->getId()]), + ]); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $chatMessageRepository->add($chatMessage, true); + + return $this->render('closeModalAndReload.html.twig'); + } + + return $this->render('genericModalForm.html.twig', [ + 'title' => "Veuillez saisir votre message", + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{id}", name="app_chat_message_delete", methods={"POST"}) + */ + public function delete(Request $request, ChatMessage $chatMessage, ChatMessageRepository $chatMessageRepository): Response + { + if ($this->isCsrfTokenValid('delete'.$chatMessage->getId(), $request->request->get('_token'))) { + $chatMessageRepository->remove($chatMessage, true); + } + + if ($chatMessage->getEntry()) { + return $this->redirectToRoute('app_entry_show', ['id' => $chatMessage->getEntry()->getId()], Response::HTTP_SEE_OTHER); + } elseif ($chatMessage->getLexicon()) { + return $this->redirectToRoute('app_lexicon_show', ['id' => $chatMessage->getLexicon()->getId()], Response::HTTP_SEE_OTHER); + } else { + return $this->redirectToRoute('app_index', [], Response::HTTP_SEE_OTHER); + } + + } +} diff --git a/src/Entity/ChatMessage.php b/src/Entity/ChatMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..1e996db19ce7ebd2dd18fa49c7408e5b2c82a93a --- /dev/null +++ b/src/Entity/ChatMessage.php @@ -0,0 +1,147 @@ +<?php + +namespace App\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use App\Repository\ChatMessageRepository; + +/** + * @ORM\Entity(repositoryClass=ChatMessageRepository::class) + * @ORM\HasLifecycleCallbacks + */ +class ChatMessage +{ + + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\Column(type="text", length=80) + * @Groups({"comment:read"}) + */ + private $content; + + /** + * @ORM\Column(type="datetime_immutable") + */ + private $createdAt; + + /** + * @ORM\Column(type="datetime_immutable", nullable=true) + */ + private $updatedAt; + + /** + * @ORM\ManyToOne(targetEntity=User::class, inversedBy="comments") + * @Groups({"comment:read"}) + */ + private $createdBy; + + /** + * @ORM\ManyToOne(targetEntity=Lexicon::class, inversedBy="comments") + */ + private $lexicon; + + /** + * @ORM\ManyToOne(targetEntity=Entry::class, inversedBy="comments") + */ + private $entry; + + /** + * @ORM\PrePersist + * @ORM\PreUpdate + */ + public function updatedTimestamps(): void + { + $this->setUpdatedAt(new \DateTimeImmutable('now')); + if ($this->getCreatedAt() === null) { + $this->setCreatedAt(new \DateTimeImmutable('now')); + } + } + + public function getId(): ?int + { + return $this->id; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(string $content): self + { + $this->content = $content; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self + { + $this->updatedAt = $updatedAt; + + return $this; + } + + public function getCreatedBy(): ?User + { + return $this->createdBy; + } + + public function setCreatedBy(?User $createdBy): self + { + $this->createdBy = $createdBy; + + return $this; + } + + public function getLexicon(): ?Lexicon + { + return $this->lexicon; + } + + public function setLexicon(?Lexicon $lexicon): self + { + $this->lexicon = $lexicon; + + return $this; + } + + public function getEntry(): ?Entry + { + return $this->entry; + } + + public function setEntry(?Entry $entry): self + { + $this->entry = $entry; + + return $this; + } + +} diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php index ab772bcaf2604c4e8b0ac3350eb18467dd1c7f91..33ef1f43e4a6bbfaa73c8957efe6d9f7b1e92045 100644 --- a/src/Entity/Entry.php +++ b/src/Entity/Entry.php @@ -116,9 +116,15 @@ class Entry */ private $logs; + /** + * @ORM\OneToMany(targetEntity=ChatMessage::class, mappedBy="entry", cascade={"remove", "persist"}) + */ + private $chatMessages; + public function __construct() { $this->logs = new ArrayCollection(); + $this->chatMessages = new ArrayCollection(); } public function __toString() @@ -329,4 +335,34 @@ class Entry { $this->addingOrder = $addingOrder; } + + /** + * @return Collection<int, ChatMessage> + */ + public function getChatMessages(): Collection + { + return $this->chatMessages; + } + + public function addChatMessage(ChatMessage $chatMessage): self + { + if (!$this->chatMessages->contains($chatMessage)) { + $this->chatMessages[] = $chatMessage; + $chatMessage->setEntry($this); + } + + return $this; + } + + public function removeChatMessage(ChatMessage $chatMessage): self + { + if ($this->chatMessages->removeElement($chatMessage)) { + // set the owning side to null (unless already changed) + if ($chatMessage->getEntry() === $this) { + $chatMessage->setEntry(null); + } + } + + return $this; + } } diff --git a/src/Entity/Lexicon.php b/src/Entity/Lexicon.php index 725549bb6cb1f370334916a9b9bf40afeb4488d2..204bb4bbb6ed3c7143c8b19465573c441d5fc065 100644 --- a/src/Entity/Lexicon.php +++ b/src/Entity/Lexicon.php @@ -113,6 +113,11 @@ class Lexicon */ private $logs; + /** + * @ORM\OneToMany(targetEntity=ChatMessage::class, mappedBy="lexicon", cascade={"remove", "persist"}) + */ + private $chatMessages; + /** * @ORM\OneToMany(targetEntity=LabelVisibility::class, mappedBy="lexicon") */ @@ -135,6 +140,7 @@ class Lexicon $this->graphyLists = new ArrayCollection(); $this->logs = new ArrayCollection(); $this->labelVisibilities = new ArrayCollection(); + $this->chatMessages = new ArrayCollection(); } public function isZero() @@ -442,4 +448,34 @@ class Lexicon return $this; } + + /** + * @return Collection<int, ChatMessage> + */ + public function getChatMessages(): Collection + { + return $this->chatMessages; + } + + public function addChatMessage(ChatMessage $chatMessage): self + { + if (!$this->chatMessages->contains($chatMessage)) { + $this->chatMessages[] = $chatMessage; + $chatMessage->setLexicon($this); + } + + return $this; + } + + public function removeChatMessage(ChatMessage $chatMessage): self + { + if ($this->chatMessages->removeElement($chatMessage)) { + // set the owning side to null (unless already changed) + if ($chatMessage->getLexicon() === $this) { + $chatMessage->setLexicon(null); + } + } + + return $this; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index d3db999c5e2ff327a598400151727aad5b88f8a8..0509e19626e9d5a2fc775fd89f83c802f5c586f8 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -232,6 +232,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface */ private $headwordUserInfos; + /** + * @ORM\OneToMany(targetEntity=ChatMessage::class, mappedBy="createdBy", cascade={"remove", "persist"}) + */ + private $chatMessages; + public function __toString() { return $this->getPseudo(); @@ -260,6 +265,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->labelVisibilities = new ArrayCollection(); $this->entries = new ArrayCollection(); $this->headwordUserInfos = new ArrayCollection(); + $this->chatMessages = new ArrayCollection(); } public function getInitials() @@ -1106,4 +1112,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection<int, ChatMessage> + */ + public function getChatMessages(): Collection + { + return $this->chatMessages; + } + + public function addChatMessage(ChatMessage $chatMessage): self + { + if (!$this->chatMessages->contains($chatMessage)) { + $this->chatMessages[] = $chatMessage; + $chatMessage->setCreatedBy($this); + } + + return $this; + } + + public function removeChatMessage(ChatMessage $chatMessage): self + { + if ($this->chatMessages->removeElement($chatMessage)) { + // set the owning side to null (unless already changed) + if ($chatMessage->getCreatedBy() === $this) { + $chatMessage->setCreatedBy(null); + } + } + + return $this; + } } diff --git a/src/Entity/Vote.php b/src/Entity/Vote.php index 5ea5b3d842fef52713af427e5555f703f90e56e0..3682e1e845a4f53c72c4904d56c8f9fc98981649 100644 --- a/src/Entity/Vote.php +++ b/src/Entity/Vote.php @@ -7,6 +7,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use App\Repository\VoteRepository; /** * @ORM\Entity(repositoryClass=VoteRepository::class) diff --git a/src/Form/ChatMessageType.php b/src/Form/ChatMessageType.php new file mode 100644 index 0000000000000000000000000000000000000000..6793eabaadb0c435b0f486e18b335f125cba7514 --- /dev/null +++ b/src/Form/ChatMessageType.php @@ -0,0 +1,45 @@ +<?php + +namespace App\Form; + +use App\Entity\ChatMessage; +use App\Entity\Label; +use App\Entity\User; +use App\Languages\LanguagesIso; +use App\Repository\GroupRepository; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ChatMessageType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('content', TextareaType::class, [ + 'label' => false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'Enregistrer', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ChatMessage::class, + 'attr' => [ + 'data-ajax-form' => '', + 'data-ajax-form-target' => '#bootstrap-modal .modal-content', + 'novalidate' => 'novalidate', + ], + ]); + } +} diff --git a/src/Repository/ChatMessageRepository.php b/src/Repository/ChatMessageRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..c9a44e0725998958bf2b241c418c46f43e08baec --- /dev/null +++ b/src/Repository/ChatMessageRepository.php @@ -0,0 +1,66 @@ +<?php + +namespace App\Repository; + +use App\Entity\ChatMessage; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Comment> + * + * @method ChatMessage|null find($id, $lockMode = null, $lockVersion = null) + * @method ChatMessage|null findOneBy(array $criteria, array $orderBy = null) + * @method ChatMessage[] findAll() + * @method ChatMessage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ChatMessageRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ChatMessage::class); + } + + public function add(ChatMessage $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(ChatMessage $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + +// /** +// * @return Comment[] Returns an array of Comment objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('l') +// ->andWhere('l.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('l.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Comment +// { +// return $this->createQueryBuilder('l') +// ->andWhere('l.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/templates/chat_message/_delete_form.html.twig b/templates/chat_message/_delete_form.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..86e70a35ae4ffd7a45aa966d69c43ac99c5ce341 --- /dev/null +++ b/templates/chat_message/_delete_form.html.twig @@ -0,0 +1,4 @@ +<form method="post" action="{{ path('app_chat_message_delete', {'id': chatMessage.id}) }}" onsubmit="return confirm('{{ "Confirmer la suppression ?"|trans }}');"> + <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ chatMessage.id) }}"> + <button type="submit" class="btn btn-link"><i title="{{ "Supprimer"|trans }}" class="fa fa-trash"></i></button> +</form> diff --git a/templates/entry/show.html.twig b/templates/entry/show.html.twig index 2033cf185404c262dfd7c608b0ee6ddbfdbbcd88..fcc67b7d630d0c2865c12c8de1de227a0c0923ee 100644 --- a/templates/entry/show.html.twig +++ b/templates/entry/show.html.twig @@ -59,13 +59,58 @@ </div> <div class="col col-md-3 col-sm-12"> - <div id="history" class="grey-panel"> - <h5>{{ "Historique des modifications"|trans }}</h5> - {% for log in log_manager.lastLogsForEntry(entry, app.user) %} - <p>{% include "log/_formattedLog.html.twig" %}</p> - {% endfor %} - </div> - </div> + + <div id="history" class="grey-panel"> + <h5>{{ "Discussion"|trans }}</h5> + + <div id="chatMessages"> + {% for chatMessage in entry.chatMessages %} + <div class=" chat-message {{ chatMessage.createdBy == app.user ? 'chat-message-own' }}"> + + <div class="d-flex justify-content-start"> + + <div class=" flex-shrink-0 mt-1 me-2">{% if app.user != chatMessage.createdBy %}{{ app.user|badge }}{% endif %}</div> + + <div class="chat-message-content"> + <div class="chat-message-body"> + {{ chatMessage.content|nl2br|raw }} + </div> + + <div class="chat-message-footer"> + {{ chatMessage.createdAt|date('d/m/Y Ã H:i') }} + {% if chatMessage.createdBy == app.user %} + <a title="{{ "Modifier"|trans }}" href="#" data-url="{{ path('app_chat_message_edit', {id: chatMessage.id}) }}" class="modal-form"> + <i class="fa fa-pencil"></i> + </a> + <div class="d-inline-block">{{ include('chat_message/_delete_form.html.twig') }}</div> + {% endif %} + </div> + + </div> + </div> + + </div> + {% endfor %} + </div> + + <div class="row"> + <div class="col-md-12"> + <a title="{{ "Ajouter un message"|trans }}" href="#" data-url="{{ path('app_chat_message_new', {entryId: entry.id}) }}" class="modal-form"> + <i class="fa fa-plus-circle text-success"></i> {{ "Ajouter un message"|trans }} + </a> + </div> + </div> + + </div> + + <div id="history" class="grey-panel"> + <h5 class="mb-3">{{ "Historique des modifications"|trans }}</h5> + {% for log in log_manager.lastLogsForEntry(entry, app.user) %} + <p>{% include "log/_formattedLog.html.twig" %}</p> + {% endfor %} + </div> + + </div> </div> diff --git a/templates/label/_labelBadge.html.twig b/templates/label/_labelBadge.html.twig index e1fbcf72a71d6e7e03561152a8787eeb4b032296..836bf5cb07bf701115d9f4eed65dab13ced4b565 100644 --- a/templates/label/_labelBadge.html.twig +++ b/templates/label/_labelBadge.html.twig @@ -1,9 +1,9 @@ <div class="badge rounded-pill {{ label.isMilestone ? 'badge-milestone'}} position-relative text-black {{ label|labelClass }} {{ label.isMilestone or label.isInstitutional or label.isMasterPublic ? 'border-auto'}}" style="margin-right: 5px;" - title="{{ label.description ? label.description~'.' : }} Updated: {{ label.updatedAt|date('d/m/Y') }}"> + title="{{ label.description ? label.description~'.' }} Updated: {{ label.updatedAt|date('d/m/Y') }}"> {{ label }} {% if label.isMilestone %}<br>{{ label.getMilestone|date('d/m/Y') }}{% endif %} {% if showHeadwordsCounter|default(false) %} <span class="position-absolute top-100 start-100 text-black">{{ label_manager.getHeadwordsNbWithLabel(label, app.user) }}/{{ label_manager.getHeadwordsNbWithLabel(label) }}</span> {% endif %} -</div> \ No newline at end of file +</div> diff --git a/templates/lexicon/show.html.twig b/templates/lexicon/show.html.twig index 4f5bffa9cd6761d159133ee7ca12c1537db3df7d..34f2d953562d0985fc66581ec9a50d25ecf5581d 100644 --- a/templates/lexicon/show.html.twig +++ b/templates/lexicon/show.html.twig @@ -125,7 +125,7 @@ <div class="col col-md-3"> <div id="history" class="grey-panel"> - <h5>{{ "Historique des modifications"|trans }}</h5> + <h5 class="mb-3">{{ "Historique des modifications"|trans }}</h5> {% for log in log_manager.lastLogsForLexicon(lexicon, app.user) %} <p>{% include "log/_formattedLog.html.twig" %}</p> {% endfor %}