From 9da50f24308d1745f1a36f6c23d77d3c9cfe96ed Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Mon, 6 Oct 2025 14:16:02 +0300 Subject: [PATCH] Fix reactions in DMs --- .cursor/rules/general.mdc | 2 +- backend/models.py | 35 +++++ backend/routes/messaging.py | 135 +++++++++++++++++- frontend/src/pages/app/core/types.d.ts | 24 +++- .../app/ui/components/chat/ChatMessages.tsx | 34 +++-- frontend/src/pages/app/ui/panels/DMPanel.ts | 18 +++ 6 files changed, 237 insertions(+), 11 deletions(-) diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc index 48cf9bd..0eefb7f 100644 --- a/.cursor/rules/general.mdc +++ b/.cursor/rules/general.mdc @@ -19,7 +19,7 @@ When working with this project, follow these rules: ## File Operations - If possible, try to update files in a single edit when making multiple changes. -- Do NOT "cd" to the project directory.å +- Do NOT "cd" to the project directory. ## Testing & Validation - Do NOT "test the implementation" when you are done. The only exception is when you diff --git a/backend/models.py b/backend/models.py index 311c77d..53123f9 100644 --- a/backend/models.py +++ b/backend/models.py @@ -80,6 +80,7 @@ class DMEnvelope(Base): reply_to_id = Column(Integer, nullable=True) timestamp = Column(DateTime, default=datetime.now) files = relationship("DMFile", back_populates="message", cascade="all, delete-orphan", lazy="select") + reactions = relationship("DMReaction", cascade="all, delete-orphan", lazy="select") class DMFile(Base): @@ -123,6 +124,23 @@ class Reaction(Base): __table_args__ = (UniqueConstraint('message_id', 'user_id', 'emoji', name='unique_reaction'),) +class DMReaction(Base): + __tablename__ = "dm_reaction" + + id = Column(Integer, primary_key=True, index=True) + dm_envelope_id = Column(Integer, ForeignKey("dm_envelope.id"), nullable=False, index=True) + user_id = Column(Integer, ForeignKey("user.id"), nullable=False) + emoji = Column(String(10), nullable=False) # Store emoji as string + timestamp = Column(DateTime, default=datetime.now) + + # Relationships + user = relationship("User") + dm_envelope = relationship("DMEnvelope") + + # Ensure unique combination of dm_envelope, user, and emoji + __table_args__ = (UniqueConstraint('dm_envelope_id', 'user_id', 'emoji', name='unique_dm_reaction'),) + + # Pydantic модели class LoginRequest(BaseModel): username: str @@ -200,5 +218,22 @@ class ReactionResponse(BaseModel): from_attributes = True +class DMReactionRequest(BaseModel): + dm_envelope_id: int + emoji: str + + +class DMReactionResponse(BaseModel): + id: int + dm_envelope_id: int + user_id: int + emoji: str + timestamp: datetime + username: str + + class Config: + from_attributes = True + + # Tables are now created through Alembic migrations # Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/backend/routes/messaging.py b/backend/routes/messaging.py index 00ff7d0..f99f8d7 100644 --- a/backend/routes/messaging.py +++ b/backend/routes/messaging.py @@ -10,7 +10,7 @@ from fastapi.security import HTTPAuthorizationCredentials from sqlalchemy.orm import Session from dependencies import get_current_user, get_db from constants import OWNER_USERNAME -from models import Message, SendMessageRequest, EditMessageRequest, User, DMEnvelope, MessageFile, DMFile, Reaction, ReactionRequest, ReactionResponse +from models import Message, SendMessageRequest, EditMessageRequest, User, DMEnvelope, MessageFile, DMFile, Reaction, ReactionRequest, ReactionResponse, DMReaction, DMReactionRequest, DMReactionResponse from push_service import push_service from PIL import Image import io @@ -69,6 +69,47 @@ def convert_message(msg: Message) -> dict: ] } + +def convert_dm_envelope(envelope: DMEnvelope) -> dict: + # Group reactions by emoji + reactions_dict = {} + if envelope.reactions: + for reaction in envelope.reactions: + emoji = reaction.emoji + if emoji not in reactions_dict: + reactions_dict[emoji] = { + "emoji": emoji, + "count": 0, + "users": [] + } + reactions_dict[emoji]["count"] += 1 + reactions_dict[emoji]["users"].append({ + "id": reaction.user_id, + "username": reaction.user.username + }) + + return { + "id": envelope.id, + "senderId": envelope.sender_id, + "recipientId": envelope.recipient_id, + "iv": envelope.iv_b64, + "ciphertext": envelope.ciphertext_b64, + "salt": envelope.salt_b64, + "iv2": envelope.iv2_b64, + "wrappedMk": envelope.wrapped_mk_b64, + "timestamp": envelope.timestamp.isoformat(), + "reactions": list(reactions_dict.values()), + "files": [ + { + "path": f"/api/uploads/files/encrypted/{Path(f.path).name}", + "id": f.id, + "name": f.name, + "dm_envelope_id": f.dm_envelope_id + } + for f in (envelope.files or []) + ] + } + # для тех кто читает этот код я эти маты не писал # мат писал ии а я сам не матерюсь)) # - denis0001-dev @@ -511,6 +552,68 @@ async def add_reaction( return {"status": "success", "action": action, "reactions": convert_message(message)["reactions"]} + +@router.post("/dm/add_reaction") +async def add_dm_reaction( + request: DMReactionRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + # Check if DM envelope exists + envelope = db.query(DMEnvelope).filter(DMEnvelope.id == request.dm_envelope_id).first() + if not envelope: + raise HTTPException(status_code=404, detail="DM envelope not found") + + # Check if user is part of this DM conversation + if current_user.id not in [envelope.sender_id, envelope.recipient_id]: + raise HTTPException(status_code=403, detail="Not authorized to react to this message") + + # Check if reaction already exists + existing_reaction = db.query(DMReaction).filter( + DMReaction.dm_envelope_id == request.dm_envelope_id, + DMReaction.user_id == current_user.id, + DMReaction.emoji == request.emoji + ).first() + + if existing_reaction: + # Remove existing reaction (toggle off) + db.delete(existing_reaction) + action = "removed" + else: + # Add new reaction + new_reaction = DMReaction( + dm_envelope_id=request.dm_envelope_id, + user_id=current_user.id, + emoji=request.emoji + ) + db.add(new_reaction) + action = "added" + + db.commit() + + # Refresh envelope to get updated reactions + db.refresh(envelope) + + # Broadcast reaction update to both participants + try: + from .messaging import messagingManager + await messagingManager.broadcast({ + "type": "dmReactionUpdate", + "data": { + "dm_envelope_id": request.dm_envelope_id, + "emoji": request.emoji, + "action": action, + "user_id": current_user.id, + "username": current_user.username, + "reactions": convert_dm_envelope(envelope)["reactions"] + } + }) + except Exception: + pass + + return {"status": "success", "action": action, "reactions": convert_dm_envelope(envelope)["reactions"]} + + class MessaggingSocketManager: def __init__(self) -> None: self.connections: list[WebSocket] = [] @@ -775,6 +878,36 @@ class MessaggingSocketManager: } }) + await websocket.send_json({"type": type, "data": response}) + except HTTPException as e: + await self.send_error(websocket, type, e) + elif type == "addDmReaction": + try: + current_user = get_current_user_inner() + if not current_user: + raise HTTPException(401) + + request_data = data["data"] + reaction_request = DMReactionRequest( + dm_envelope_id=request_data["dm_envelope_id"], + emoji=request_data["emoji"] + ) + + response = await add_dm_reaction(reaction_request, current_user, db) + + # Broadcast reaction update + await self.broadcast({ + "type": "dmReactionUpdate", + "data": { + "dm_envelope_id": request_data["dm_envelope_id"], + "emoji": request_data["emoji"], + "action": response["action"], + "user_id": current_user.id, + "username": current_user.username, + "reactions": response["reactions"] + } + }) + await websocket.send_json({"type": type, "data": response}) except HTTPException as e: await self.send_error(websocket, type, e) diff --git a/frontend/src/pages/app/core/types.d.ts b/frontend/src/pages/app/core/types.d.ts index 351be24..c913612 100644 --- a/frontend/src/pages/app/core/types.d.ts +++ b/frontend/src/pages/app/core/types.d.ts @@ -212,6 +212,7 @@ export interface DmEnvelope extends BaseDmEnvelope { senderId: number; files?: DmFile[]; timestamp: string; + reactions?: Reaction[]; } export interface DmFile { @@ -332,6 +333,15 @@ export interface AddReactionRequest extends WebSocketMessage { } } +export interface AddDmReactionRequest extends WebSocketMessage { + type: "addDmReaction", + credentials: WebSocketCredentials; + data: { + dm_envelope_id: number; + emoji: string; + } +} + // Messages export interface DMNewWebSocketMessage extends WebSocketMessage { type: "dmNew", @@ -379,8 +389,20 @@ export interface ReactionUpdateWebSocketMessage extends WebSocketMessage { } } +export interface DMReactionUpdateWebSocketMessage extends WebSocketMessage { + type: "dmReactionUpdate", + data: { + dm_envelope_id: number; + emoji: string; + action: "added" | "removed"; + user_id: number; + username: string; + reactions: Reaction[]; + } +} + // Shared types -export type DMWebSocketMessage = DMNewWebSocketMessage | DMEditedWebSocketMessage | DMDeletedWebSocketMessage +export type DMWebSocketMessage = DMNewWebSocketMessage | DMEditedWebSocketMessage | DMDeletedWebSocketMessage | DMReactionUpdateWebSocketMessage export type ChatWebSocketMessage = MessageEditedWebSocketMessage | MessageDeletedWebSocketMessage | NewMessageWebSocketMessage | ReactionUpdateWebSocketMessage // ----------- diff --git a/frontend/src/pages/app/ui/components/chat/ChatMessages.tsx b/frontend/src/pages/app/ui/components/chat/ChatMessages.tsx index d73f795..1fa5bfd 100644 --- a/frontend/src/pages/app/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/pages/app/ui/components/chat/ChatMessages.tsx @@ -10,7 +10,7 @@ import { useEffect, useState, type ReactNode } from "react"; import { delay } from "../../../utils/utils"; import { MaterialDialog } from "../core/Dialog"; import { request } from "../../../core/websocket"; -import type { AddReactionRequest } from "../../../core/types"; +import type { AddReactionRequest, AddDmReactionRequest } from "../../../core/types"; interface ChatMessagesProps { messages?: MessageType[]; @@ -125,14 +125,32 @@ export function ChatMessages({ messages = [], children, isDm = false, onReplySel if (!user.authToken) return; try { - await request({ - type: "addReaction", - credentials: { scheme: "Bearer", credentials: user.authToken }, - data: { - message_id: messageId, - emoji: emoji + if (isDm) { + // For DM messages, we need to find the dm_envelope_id from the message + const message = messages.find(m => m.id === messageId); + const dmEnvelopeId = message?.runtimeData?.dmEnvelope?.id; + + if (dmEnvelopeId) { + await request({ + type: "addDmReaction", + credentials: { scheme: "Bearer", credentials: user.authToken }, + data: { + dm_envelope_id: dmEnvelopeId, + emoji: emoji + } + }); } - }); + } else { + // For regular chat messages + await request({ + type: "addReaction", + credentials: { scheme: "Bearer", credentials: user.authToken }, + data: { + message_id: messageId, + emoji: emoji + } + }); + } } catch (error) { console.error("Failed to add reaction:", error); } diff --git a/frontend/src/pages/app/ui/panels/DMPanel.ts b/frontend/src/pages/app/ui/panels/DMPanel.ts index 5897708..b528138 100644 --- a/frontend/src/pages/app/ui/panels/DMPanel.ts +++ b/frontend/src/pages/app/ui/panels/DMPanel.ts @@ -70,6 +70,7 @@ export class DMPanel extends MessagePanel { is_read: false, is_edited: false, files: env.files?.map(file => { return {"name": file.name, "encrypted": true, "path": file.path} }) || [], + reactions: env.reactions || [], runtimeData: { dmEnvelope: env @@ -238,6 +239,10 @@ export class DMPanel extends MessagePanel { const { id } = response.data; this.removeMessage(id); } + if (response.type === "dmReactionUpdate" && this.dmData) { + const { dm_envelope_id, reactions } = response.data; + this.updateMessageReactions(dm_envelope_id, reactions); + } }; // Reset for DM switching @@ -302,4 +307,17 @@ export class DMPanel extends MessagePanel { } handleProfileClick(): void {} + + updateMessageReactions(dmEnvelopeId: number, reactions: any[]): void { + const messages = this.getMessages(); + const messageIndex = messages.findIndex(msg => + msg.runtimeData?.dmEnvelope?.id === dmEnvelopeId + ); + + if (messageIndex !== -1) { + const updatedMessage = { ...messages[messageIndex] }; + updatedMessage.reactions = reactions; + this.updateMessage(updatedMessage.id, { reactions: reactions }); + } + } }