Fix reactions in DMs

This commit is contained in:
2025-10-06 14:16:02 +03:00
Unverified
parent d6b2849e46
commit 9da50f2430
6 changed files with 237 additions and 11 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ When working with this project, follow these rules:
## File Operations ## File Operations
- If possible, try to update files in a single edit when making multiple changes. - 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 ## Testing & Validation
- Do NOT "test the implementation" when you are done. The only exception is when you - Do NOT "test the implementation" when you are done. The only exception is when you
+35
View File
@@ -80,6 +80,7 @@ class DMEnvelope(Base):
reply_to_id = Column(Integer, nullable=True) reply_to_id = Column(Integer, nullable=True)
timestamp = Column(DateTime, default=datetime.now) timestamp = Column(DateTime, default=datetime.now)
files = relationship("DMFile", back_populates="message", cascade="all, delete-orphan", lazy="select") files = relationship("DMFile", back_populates="message", cascade="all, delete-orphan", lazy="select")
reactions = relationship("DMReaction", cascade="all, delete-orphan", lazy="select")
class DMFile(Base): class DMFile(Base):
@@ -123,6 +124,23 @@ class Reaction(Base):
__table_args__ = (UniqueConstraint('message_id', 'user_id', 'emoji', name='unique_reaction'),) __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 модели # Pydantic модели
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
username: str username: str
@@ -200,5 +218,22 @@ class ReactionResponse(BaseModel):
from_attributes = True 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 # Tables are now created through Alembic migrations
# Base.metadata.create_all(bind=engine) # Base.metadata.create_all(bind=engine)
+134 -1
View File
@@ -10,7 +10,7 @@ from fastapi.security import HTTPAuthorizationCredentials
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from dependencies import get_current_user, get_db from dependencies import get_current_user, get_db
from constants import OWNER_USERNAME 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 push_service import push_service
from PIL import Image from PIL import Image
import io 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 # - denis0001-dev
@@ -511,6 +552,68 @@ async def add_reaction(
return {"status": "success", "action": action, "reactions": convert_message(message)["reactions"]} 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: class MessaggingSocketManager:
def __init__(self) -> None: def __init__(self) -> None:
self.connections: list[WebSocket] = [] 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}) await websocket.send_json({"type": type, "data": response})
except HTTPException as e: except HTTPException as e:
await self.send_error(websocket, type, e) await self.send_error(websocket, type, e)
+23 -1
View File
@@ -212,6 +212,7 @@ export interface DmEnvelope extends BaseDmEnvelope {
senderId: number; senderId: number;
files?: DmFile[]; files?: DmFile[];
timestamp: string; timestamp: string;
reactions?: Reaction[];
} }
export interface DmFile { 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 // Messages
export interface DMNewWebSocketMessage extends WebSocketMessage { export interface DMNewWebSocketMessage extends WebSocketMessage {
type: "dmNew", 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 // Shared types
export type DMWebSocketMessage = DMNewWebSocketMessage | DMEditedWebSocketMessage | DMDeletedWebSocketMessage export type DMWebSocketMessage = DMNewWebSocketMessage | DMEditedWebSocketMessage | DMDeletedWebSocketMessage | DMReactionUpdateWebSocketMessage
export type ChatWebSocketMessage = MessageEditedWebSocketMessage | MessageDeletedWebSocketMessage | NewMessageWebSocketMessage | ReactionUpdateWebSocketMessage export type ChatWebSocketMessage = MessageEditedWebSocketMessage | MessageDeletedWebSocketMessage | NewMessageWebSocketMessage | ReactionUpdateWebSocketMessage
// ----------- // -----------
@@ -10,7 +10,7 @@ import { useEffect, useState, type ReactNode } from "react";
import { delay } from "../../../utils/utils"; import { delay } from "../../../utils/utils";
import { MaterialDialog } from "../core/Dialog"; import { MaterialDialog } from "../core/Dialog";
import { request } from "../../../core/websocket"; import { request } from "../../../core/websocket";
import type { AddReactionRequest } from "../../../core/types"; import type { AddReactionRequest, AddDmReactionRequest } from "../../../core/types";
interface ChatMessagesProps { interface ChatMessagesProps {
messages?: MessageType[]; messages?: MessageType[];
@@ -125,6 +125,23 @@ export function ChatMessages({ messages = [], children, isDm = false, onReplySel
if (!user.authToken) return; if (!user.authToken) return;
try { try {
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<AddDmReactionRequest["data"], any>({
type: "addDmReaction",
credentials: { scheme: "Bearer", credentials: user.authToken },
data: {
dm_envelope_id: dmEnvelopeId,
emoji: emoji
}
});
}
} else {
// For regular chat messages
await request<AddReactionRequest["data"], any>({ await request<AddReactionRequest["data"], any>({
type: "addReaction", type: "addReaction",
credentials: { scheme: "Bearer", credentials: user.authToken }, credentials: { scheme: "Bearer", credentials: user.authToken },
@@ -133,6 +150,7 @@ export function ChatMessages({ messages = [], children, isDm = false, onReplySel
emoji: emoji emoji: emoji
} }
}); });
}
} catch (error) { } catch (error) {
console.error("Failed to add reaction:", error); console.error("Failed to add reaction:", error);
} }
@@ -70,6 +70,7 @@ export class DMPanel extends MessagePanel {
is_read: false, is_read: false,
is_edited: false, is_edited: false,
files: env.files?.map(file => { return {"name": file.name, "encrypted": true, "path": file.path} }) || [], files: env.files?.map(file => { return {"name": file.name, "encrypted": true, "path": file.path} }) || [],
reactions: env.reactions || [],
runtimeData: { runtimeData: {
dmEnvelope: env dmEnvelope: env
@@ -238,6 +239,10 @@ export class DMPanel extends MessagePanel {
const { id } = response.data; const { id } = response.data;
this.removeMessage(id); 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 // Reset for DM switching
@@ -302,4 +307,17 @@ export class DMPanel extends MessagePanel {
} }
handleProfileClick(): void {} 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 });
}
}
} }