From b1f88ca501b85b76a032389e4c6e1e5e062dab17 Mon Sep 17 00:00:00 2001 From: David Bruno Vontor Date: Thu, 4 Jun 2026 16:45:16 +0200 Subject: [PATCH] Restore reactions, add constraint, handle errors Restore soft-deleted message reactions and enforce uniqueness only for active reactions; add WebSocket error handling and minor UI/Docker tweaks. - backend/social/chat/models.py: Toggle reaction now restores a stale soft-deleted MessageReaction (avoids unique conflicts) and creates new reactions as needed. Replaced unique_together with a conditional UniqueConstraint that applies only to non-deleted records. - backend/social/chat/consumers.py: Wrap reaction toggle in try/except to return a WS error message on failure instead of allowing exceptions to bubble up. - frontend/src/components/social/chat/Message.tsx: Adjusted Tailwind max-width class for the reaction menu (max-w-32). - docker-compose.yml: Added commented example configuration for an optional Janus media server (documentational/commented service). These changes prevent unique constraint errors when restoring reactions, improve robustness of the WebSocket reaction flow, and include small UI and deployment notes. --- backend/social/chat/consumers.py | 15 ++++++---- backend/social/chat/models.py | 30 ++++++++++++------- docker-compose.yml | 15 ++++++++++ .../src/components/social/chat/Message.tsx | 2 +- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/backend/social/chat/consumers.py b/backend/social/chat/consumers.py index be15191..2b3d190 100644 --- a/backend/social/chat/consumers.py +++ b/backend/social/chat/consumers.py @@ -74,11 +74,16 @@ class ChatConsumer(AsyncWebsocketConsumer): }) elif msg_type == "reaction": - action, reaction = await _toggle_reaction( - message_id=data["message_id"], - user=user, - emoji=data["emoji"], - ) + try: + action, _reaction = await _toggle_reaction( + message_id=data["message_id"], + user=user, + emoji=data["emoji"], + ) + except Exception: + await self.send(text_data=json.dumps({"error": "Reaction failed."})) + return + await self.channel_layer.group_send(self.chat_name, { "type": "message.reaction", "message_id": data["message_id"], diff --git a/backend/social/chat/models.py b/backend/social/chat/models.py index 9834462..944371b 100644 --- a/backend/social/chat/models.py +++ b/backend/social/chat/models.py @@ -142,24 +142,26 @@ class Message(SoftDeleteModel): """ try: reaction = MessageReaction.objects.get(message=self, user=user) - + if reaction.emoji == emoji: - # Same emoji -> Remove it (Toggle) reaction.delete() return 'removed', None else: - # Different emoji -> Switch it reaction.emoji = emoji reaction.save() return 'switched', reaction - + except MessageReaction.DoesNotExist: - # New reaction -> Create it - reaction = MessageReaction.objects.create( - message=self, - user=user, - emoji=emoji - ) + # Restore a stale soft-deleted record if one exists (avoids unique_together violation). + stale = MessageReaction.all_objects.filter(message=self, user=user).first() + if stale: + stale.emoji = emoji + stale.is_deleted = False + stale.deleted_at = None + stale.save() + return 'added', stale + + reaction = MessageReaction.objects.create(message=self, user=user, emoji=emoji) return 'added', reaction def __str__(self): @@ -190,7 +192,13 @@ class MessageReaction(SoftDeleteModel): created_at = models.DateTimeField(auto_now_add=True) class Meta: - unique_together = ('message', 'user') + constraints = [ + models.UniqueConstraint( + fields=["message", "user"], + condition=models.Q(is_deleted=False), + name="unique_active_reaction_per_user_message", + ) + ] def __str__(self): return f"{self.user} reacted {self.emoji}" diff --git a/docker-compose.yml b/docker-compose.yml index b2e4ebb..27aeaf3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,6 +87,21 @@ services: networks: - app_network + # janus-media-server: #WebRTC media server for handling real-time audio/video streaming + # container_name: janus-vontor-cz + # image: meetecho/janus-gateway:latest + # restart: always + # env_file: + # - ./backend/.env + # ports: + # - "8088:8088" # HTTP API + # - "8188:8188" # WebSocket API + # - "10000-10200:10000-10200/udp" # Media ports (UDP) + # networks: + # - app_network + + #https://github.com/meetecho/janus-gateway + #end of backend services ----------------------- nginx: #web server, reverse proxy, serves static files diff --git a/frontend/src/components/social/chat/Message.tsx b/frontend/src/components/social/chat/Message.tsx index 9454797..552ab84 100644 --- a/frontend/src/components/social/chat/Message.tsx +++ b/frontend/src/components/social/chat/Message.tsx @@ -169,7 +169,7 @@ export default function Message({ message, chat, onReply, onReact, highlighted,