mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Fix reactions in DMs
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,14 +125,32 @@ export function ChatMessages({ messages = [], children, isDm = false, onReplySel
|
|||||||
if (!user.authToken) return;
|
if (!user.authToken) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await request<AddReactionRequest["data"], any>({
|
if (isDm) {
|
||||||
type: "addReaction",
|
// For DM messages, we need to find the dm_envelope_id from the message
|
||||||
credentials: { scheme: "Bearer", credentials: user.authToken },
|
const message = messages.find(m => m.id === messageId);
|
||||||
data: {
|
const dmEnvelopeId = message?.runtimeData?.dmEnvelope?.id;
|
||||||
message_id: messageId,
|
|
||||||
emoji: emoji
|
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>({
|
||||||
|
type: "addReaction",
|
||||||
|
credentials: { scheme: "Bearer", credentials: user.authToken },
|
||||||
|
data: {
|
||||||
|
message_id: messageId,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user