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.
This commit is contained in:
@@ -74,11 +74,16 @@ class ChatConsumer(AsyncWebsocketConsumer):
|
|||||||
})
|
})
|
||||||
|
|
||||||
elif msg_type == "reaction":
|
elif msg_type == "reaction":
|
||||||
action, reaction = await _toggle_reaction(
|
try:
|
||||||
|
action, _reaction = await _toggle_reaction(
|
||||||
message_id=data["message_id"],
|
message_id=data["message_id"],
|
||||||
user=user,
|
user=user,
|
||||||
emoji=data["emoji"],
|
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, {
|
await self.channel_layer.group_send(self.chat_name, {
|
||||||
"type": "message.reaction",
|
"type": "message.reaction",
|
||||||
"message_id": data["message_id"],
|
"message_id": data["message_id"],
|
||||||
|
|||||||
@@ -144,22 +144,24 @@ class Message(SoftDeleteModel):
|
|||||||
reaction = MessageReaction.objects.get(message=self, user=user)
|
reaction = MessageReaction.objects.get(message=self, user=user)
|
||||||
|
|
||||||
if reaction.emoji == emoji:
|
if reaction.emoji == emoji:
|
||||||
# Same emoji -> Remove it (Toggle)
|
|
||||||
reaction.delete()
|
reaction.delete()
|
||||||
return 'removed', None
|
return 'removed', None
|
||||||
else:
|
else:
|
||||||
# Different emoji -> Switch it
|
|
||||||
reaction.emoji = emoji
|
reaction.emoji = emoji
|
||||||
reaction.save()
|
reaction.save()
|
||||||
return 'switched', reaction
|
return 'switched', reaction
|
||||||
|
|
||||||
except MessageReaction.DoesNotExist:
|
except MessageReaction.DoesNotExist:
|
||||||
# New reaction -> Create it
|
# Restore a stale soft-deleted record if one exists (avoids unique_together violation).
|
||||||
reaction = MessageReaction.objects.create(
|
stale = MessageReaction.all_objects.filter(message=self, user=user).first()
|
||||||
message=self,
|
if stale:
|
||||||
user=user,
|
stale.emoji = emoji
|
||||||
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
|
return 'added', reaction
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -190,7 +192,13 @@ class MessageReaction(SoftDeleteModel):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
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):
|
def __str__(self):
|
||||||
return f"{self.user} reacted {self.emoji}"
|
return f"{self.user} reacted {self.emoji}"
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- app_network
|
- 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 -----------------------
|
#end of backend services -----------------------
|
||||||
|
|
||||||
nginx: #web server, reverse proxy, serves static files
|
nginx: #web server, reverse proxy, serves static files
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export default function Message({ message, chat, onReply, onReact, highlighted,
|
|||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"flex items-center gap-0.5 overflow-hidden transition-[max-width] duration-200 ease-out",
|
"flex items-center gap-0.5 overflow-hidden transition-[max-width] duration-200 ease-out",
|
||||||
menuOpen ? "max-w-[8rem]" : "max-w-0",
|
menuOpen ? "max-w-32" : "max-w-0",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
Reference in New Issue
Block a user