diff --git a/assets/vue/components/message/constants.js b/assets/vue/components/message/constants.js index 31419cf4398..c93537fa52b 100644 --- a/assets/vue/components/message/constants.js +++ b/assets/vue/components/message/constants.js @@ -9,5 +9,6 @@ export const MESSAGE_TYPE_WALL = 4; export const MESSAGE_TYPE_GROUP = 5; export const MESSAGE_TYPE_INVITATION = 6; export const MESSAGE_TYPE_CONVERSATION = 7; +export const MESSAGE_TYPE_SENDER = 8; -export const MESSAGE_STATUS_DELETED = 3; +export const MESSAGE_STATUS_SENDER_DELETED = 3; diff --git a/assets/vue/services/message.js b/assets/vue/services/message.js index 7c3a1951ba5..f402c17b1aa 100644 --- a/assets/vue/services/message.js +++ b/assets/vue/services/message.js @@ -1,5 +1,6 @@ import makeService from "./api" import baseService from "./baseService" +import axios from "axios" // MIGRATION IN PROGRESS. makeService is deprecated // if you use some method in this service you should try to refactor it with new baseService defining async functions @@ -22,7 +23,27 @@ async function countUnreadMessages(params) { return await baseService.get(`/api/messages?${queryParams}`) } +async function deleteMessageForUser(messageId, userId) { + return await axios.patch(`/api/messages/${messageId}/delete-for-user`, + { userId: userId }, + { + headers: { + 'Content-Type': 'application/json' + } + }) +} + +async function checkAndUpdateMessageStatus(messageId) { + return await axios.patch(`/api/messages/${messageId}/check-and-update-status`, {}, { + headers: { + 'Content-Type': 'application/json' + } + }) +} + export const messageService = { create, countUnreadMessages, -} + deleteMessageForUser, + checkAndUpdateMessageStatus, +}; diff --git a/assets/vue/views/message/MessageCreate.vue b/assets/vue/views/message/MessageCreate.vue index cea04fbb89b..0d11e5f7011 100644 --- a/assets/vue/views/message/MessageCreate.vue +++ b/assets/vue/views/message/MessageCreate.vue @@ -36,12 +36,18 @@ const message = ref({ const isLoading = ref(false) const onSubmit = async (messageToSend) => { + if (!messageToSend.receivers || messageToSend.receivers.length === 0) { + notification.showErrorNotification("You must add at least one recipient.") + return + } + isLoading.value = true try { await messageService.create(messageToSend) + notification.showSuccessNotification("Message sent succesfully.") } catch (error) { - notification.showErrorNotification(error) + notification.showErrorNotification(error.message || "Error sending message.") } finally { isLoading.value = false } diff --git a/assets/vue/views/message/MessageList.vue b/assets/vue/views/message/MessageList.vue index e2065e1862f..2681522fcad 100644 --- a/assets/vue/views/message/MessageList.vue +++ b/assets/vue/views/message/MessageList.vue @@ -217,7 +217,7 @@ import DataTable from "primevue/datatable" import Column from "primevue/column" import { useConfirm } from "primevue/useconfirm" import { useQuery } from "@vue/apollo-composable" -import { MESSAGE_STATUS_DELETED, MESSAGE_TYPE_INBOX } from "../../components/message/constants" +import { MESSAGE_STATUS_SENDER_DELETED, MESSAGE_TYPE_INBOX, MESSAGE_TYPE_SENDER } from "../../components/message/constants" import { GET_USER_MESSAGE_TAGS } from "../../graphql/queries/MessageTag" import { useNotification } from "../../composables/notification" import { useMessageRelUserStore } from "../../store/messageRelUserStore" @@ -226,6 +226,7 @@ import SectionHeader from "../../components/layout/SectionHeader.vue" import InputGroup from "primevue/inputgroup" import InputText from "primevue/inputtext" import BaseAppLink from "../../components/basecomponents/BaseAppLink.vue" +import { messageService } from "../../services/message" const route = useRoute() const router = useRouter() @@ -367,6 +368,8 @@ function showInbox() { "order[sendDate]": "desc", itemsPerPage: initialRowsPerPage, page: 1, + 'receivers.receiver': securityStore.user["@id"], + 'receivers.receiverType': MESSAGE_TYPE_INBOX, } loadMessages() @@ -381,6 +384,7 @@ function showInboxByTag(tag) { "order[sendDate]": "desc", itemsPerPage: initialRowsPerPage, page: 1, + 'receivers.receiverType': MESSAGE_TYPE_INBOX, } loadMessages() @@ -396,6 +400,7 @@ function showUnread() { "receivers.read": false, itemsPerPage: initialRowsPerPage, page: 1, + 'receivers.receiverType': MESSAGE_TYPE_INBOX, } loadMessages() @@ -408,6 +413,7 @@ function showSent() { fetchPayload = { sender: securityStore.user["@id"], + "receivers.receiverType": MESSAGE_TYPE_SENDER, "order[sendDate]": "desc", itemsPerPage: initialRowsPerPage, page: 1, @@ -449,17 +455,26 @@ function findMyReceiver(message) { return receivers.find(({ receiver }) => receiver["@id"] === securityStore.user["@id"]) } +function extractUserId(apiId) { + return apiId.split('/').pop() +} + async function deleteMessage(message) { try { + const userId = extractUserId(securityStore.user["@id"]) + const messageId = extractUserId(message["@id"]) + if (message.sender["@id"] === securityStore.user["@id"]) { - message.status = MESSAGE_STATUS_DELETED - await store.dispatch("message/update", message) + await messageService.deleteMessageForUser(messageId, userId) } else { const myReceiver = findMyReceiver(message) if (myReceiver) { await store.dispatch("messagereluser/del", myReceiver) } } + + await messageService.checkAndUpdateMessageStatus(messageId) + notification.showSuccessNotification(t("Message deleted")) await messageRelUserStore.findUnreadCount() loadMessages() diff --git a/src/CoreBundle/DataProvider/Extension/MessageExtension.php b/src/CoreBundle/DataProvider/Extension/MessageExtension.php index 9539016543b..5b323d282f6 100644 --- a/src/CoreBundle/DataProvider/Extension/MessageExtension.php +++ b/src/CoreBundle/DataProvider/Extension/MessageExtension.php @@ -105,7 +105,7 @@ private function addWhere(QueryBuilder $qb, string $resourceClass): void $qb->setParameters([ 'current' => $user, - 'deleted' => Message::MESSAGE_STATUS_DELETED, + 'deleted' => Message::MESSAGE_STATUS_SENDER_DELETED, // 'currentList' => [$user->getId()], 'inbox' => Message::MESSAGE_TYPE_INBOX, // 'outbox' => Message::MESSAGE_TYPE_OUTBOX, diff --git a/src/CoreBundle/Entity/Message.php b/src/CoreBundle/Entity/Message.php index 2634faf8135..d922eb94f17 100644 --- a/src/CoreBundle/Entity/Message.php +++ b/src/CoreBundle/Entity/Message.php @@ -14,6 +14,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use Chamilo\CoreBundle\Filter\SearchOrFilter; @@ -51,6 +52,20 @@ provider: MessageByGroupStateProvider::class ), new Post(securityPostDenormalize: "is_granted('CREATE', object)"), + new Patch( + uriTemplate: '/messages/{id}/delete-for-user', + inputFormats: ['json' => ['application/json']], + security: "is_granted('ROLE_USER')", + output: false, + processor: MessageProcessor::class, + ), + new Patch( + uriTemplate: '/messages/{id}/check-and-update-status', + inputFormats: ['json' => ['application/json']], + security: "is_granted('ROLE_USER')", + output: false, + processor: MessageProcessor::class, + ), ], normalizationContext: [ 'groups' => ['message:read'], @@ -62,17 +77,15 @@ processor: MessageProcessor::class, )] #[ApiFilter(filterClass: OrderFilter::class, properties: ['title', 'sendDate'])] -#[ApiFilter( - filterClass: SearchFilter::class, - properties: [ - 'msgType' => 'exact', - 'status' => 'exact', - 'sender' => 'exact', - 'receivers.receiver' => 'exact', - 'receivers.tags.tag' => 'exact', - 'parent' => 'exact', - ] -)] +#[ApiFilter(SearchFilter::class, properties: [ + 'msgType' => 'exact', + 'status' => 'exact', + 'sender' => 'exact', + 'receivers.receiver' => 'exact', + 'receivers.receiverType' => 'exact', + 'receivers.tags.tag' => 'exact', + 'parent' => 'exact', +])] #[ApiFilter( BooleanFilter::class, properties: ['receivers.read'] @@ -85,7 +98,7 @@ class Message public const MESSAGE_TYPE_INVITATION = 6; public const MESSAGE_TYPE_CONVERSATION = 7; // status - public const MESSAGE_STATUS_DELETED = 3; + public const MESSAGE_STATUS_SENDER_DELETED = 3; public const MESSAGE_STATUS_DRAFT = 4; public const MESSAGE_STATUS_INVITATION_PENDING = 5; public const MESSAGE_STATUS_INVITATION_ACCEPTED = 6; diff --git a/src/CoreBundle/Entity/MessageRelUser.php b/src/CoreBundle/Entity/MessageRelUser.php index 8e5f5a81f18..c2ce652f677 100644 --- a/src/CoreBundle/Entity/MessageRelUser.php +++ b/src/CoreBundle/Entity/MessageRelUser.php @@ -46,6 +46,7 @@ class MessageRelUser { public const TYPE_TO = 1; public const TYPE_CC = 2; + public const TYPE_SENDER = 8; #[Groups(['message_rel_user:read'])] #[ORM\Column(name: 'id', type: 'integer')] diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20200821224244.php b/src/CoreBundle/Migrations/Schema/V200/Version20200821224244.php index bd5eeed6e60..afb2c2dfde7 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20200821224244.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20200821224244.php @@ -66,7 +66,7 @@ public function up(Schema $schema): void $newTypeQueries[] = sprintf( 'UPDATE message SET status = %d WHERE msg_type = %d', - Message::MESSAGE_STATUS_DELETED, + Message::MESSAGE_STATUS_SENDER_DELETED, self::OLD_MESSAGE_STATUS_DELETED ); $newTypeQueries[] = sprintf( diff --git a/src/CoreBundle/Repository/MessageRepository.php b/src/CoreBundle/Repository/MessageRepository.php index 5d29c49be37..658fa37e1f3 100644 --- a/src/CoreBundle/Repository/MessageRepository.php +++ b/src/CoreBundle/Repository/MessageRepository.php @@ -79,7 +79,7 @@ public function findByGroupId(int $groupId) $qb->where('m.group = :groupId') ->andWhere('m.status NOT IN (:excludedStatuses)') ->setParameter('groupId', $groupId) - ->setParameter('excludedStatuses', [Message::MESSAGE_STATUS_DRAFT, Message::MESSAGE_STATUS_DELETED]) + ->setParameter('excludedStatuses', [Message::MESSAGE_STATUS_DRAFT, Message::MESSAGE_STATUS_SENDER_DELETED]) ->orderBy('m.id', 'ASC') ; @@ -304,7 +304,7 @@ public function deleteTopicAndChildren(int $groupId, int $topicId): void /** @var Message $message */ foreach ($messages as $message) { - $message->setMsgType(Message::MESSAGE_STATUS_DELETED); + $message->setMsgType(Message::MESSAGE_STATUS_SENDER_DELETED); $entityManager->persist($message); } diff --git a/src/CoreBundle/State/MessageProcessor.php b/src/CoreBundle/State/MessageProcessor.php index 24e053750f3..d245e736d7c 100644 --- a/src/CoreBundle/State/MessageProcessor.php +++ b/src/CoreBundle/State/MessageProcessor.php @@ -8,6 +8,7 @@ use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\State\ProcessorInterface; use Chamilo\CoreBundle\Entity\Message; @@ -16,6 +17,8 @@ use Chamilo\CoreBundle\Repository\ResourceNodeRepository; use Doctrine\ORM\EntityManagerInterface; use Notification; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\RequestStack; use Vich\UploaderBundle\Storage\FlysystemStorage; final class MessageProcessor implements ProcessorInterface @@ -26,6 +29,8 @@ public function __construct( private readonly FlysystemStorage $storage, private readonly EntityManagerInterface $entityManager, private readonly ResourceNodeRepository $resourceNodeRepository, + private readonly Security $security, + private readonly RequestStack $requestStack ) {} public function process($data, Operation $operation, array $uriVariables = [], array $context = []) @@ -34,6 +39,14 @@ public function process($data, Operation $operation, array $uriVariables = [], a return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } + if ($operation instanceof Patch && str_contains($operation->getUriTemplate(), 'delete-for-user')) { + return $this->processDeleteForUser($data); + } + + if ($operation instanceof Patch && str_contains($operation->getUriTemplate(), 'check-and-update-status')) { + return $this->checkAndUpdateMessageStatus($data); + } + /** @var Message $message */ $message = $this->persistProcessor->process($data, $operation, $uriVariables, $context); @@ -47,12 +60,66 @@ public function process($data, Operation $operation, array $uriVariables = [], a } } + $user = $this->security->getUser(); + if (!$user) { + throw new \LogicException('User not found.'); + } + $messageRelUser = new MessageRelUser(); + $messageRelUser->setMessage($message); + $messageRelUser->setReceiver($user); + $messageRelUser->setReceiverType(MessageRelUser::TYPE_SENDER); + $this->entityManager->persist($messageRelUser); + + if ($message->getMsgType() === Message::MESSAGE_TYPE_INBOX) { + $this->saveNotificationForInboxMessage($message); + } + $this->entityManager->flush(); - if ($operation instanceof Post) { - if (Message::MESSAGE_TYPE_INBOX === $message->getMsgType()) { - $this->saveNotificationForInboxMessage($message); - } + return $message; + } + + private function processDeleteForUser($data): Message + { + /** @var Message $message */ + $message = $data; + + $request = $this->requestStack->getCurrentRequest(); + if (!$request) { + throw new \LogicException('Cannot get current request'); + } + + $requestData = json_decode($request->getContent(), true); + if (!isset($requestData['userId'])) { + throw new \InvalidArgumentException('The field userId is required.'); + } + + $userId = $requestData['userId']; + $messageRelUserRepository = $this->entityManager->getRepository(MessageRelUser::class); + $messageRelUser = $messageRelUserRepository->findOneBy([ + 'message' => $message, + 'receiver' => $userId, + ]); + + if ($messageRelUser) { + $this->entityManager->remove($messageRelUser); + $this->entityManager->flush(); + } + + return $message; + } + + private function checkAndUpdateMessageStatus($data): Message + { + /** @var Message $message */ + $message = $data; + + $messageRelUserRepository = $this->entityManager->getRepository(MessageRelUser::class); + $remainingReceivers = $messageRelUserRepository->count(['message' => $message]); + + if ($remainingReceivers === 0) { + $message->setStatus(Message::MESSAGE_STATUS_SENDER_DELETED); + $this->entityManager->flush(); } return $message;