From 87f0b5a21d763c29d93fc19dc5b7ad284a6151ea Mon Sep 17 00:00:00 2001 From: joylessorchid Date: Tue, 24 Mar 2026 12:31:02 +0300 Subject: [PATCH] fix: chat duplication, lobby scroll, ban self-protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Chat: fix race condition where SSE delivers message before POST response, causing duplicate — now checks if real msg already exists - LobbyManager: add max-h-72 + overflow-y-auto for long participant lists - ModerationPanel: hide kick/ban buttons for yourself (useLocalParticipant), add max-h-80 + overflow scroll, show API errors instead of silencing - Moderate API: reject kick/ban when targetSessionId === host userId --- src/app/api/rooms/[roomId]/moderate/route.ts | 6 ++ src/components/lobby/LobbyManager.tsx | 6 +- src/components/room/ChatPanel.tsx | 14 ++- src/components/room/ModerationPanel.tsx | 95 ++++++++++++-------- 4 files changed, 77 insertions(+), 44 deletions(-) diff --git a/src/app/api/rooms/[roomId]/moderate/route.ts b/src/app/api/rooms/[roomId]/moderate/route.ts index 259212c..6747229 100644 --- a/src/app/api/rooms/[roomId]/moderate/route.ts +++ b/src/app/api/rooms/[roomId]/moderate/route.ts @@ -45,6 +45,9 @@ export async function POST( if (!targetSessionId) { return NextResponse.json({ error: "targetSessionId required" }, { status: 400 }); } + if (targetSessionId === session.user.id) { + return NextResponse.json({ error: "Cannot kick yourself" }, { status: 400 }); + } await roomService.removeParticipant(lkRoom, targetSessionId); await prisma.participantHistory.updateMany({ where: { roomId, sessionId: targetSessionId, leftAt: null }, @@ -57,6 +60,9 @@ export async function POST( if (!targetSessionId) { return NextResponse.json({ error: "targetSessionId required" }, { status: 400 }); } + if (targetSessionId === session.user.id) { + return NextResponse.json({ error: "Cannot ban yourself" }, { status: 400 }); + } // Find fingerprint from lobby entry or participant const lobbyEntry = await prisma.lobbyEntry.findFirst({ diff --git a/src/components/lobby/LobbyManager.tsx b/src/components/lobby/LobbyManager.tsx index 439e495..5ad538d 100644 --- a/src/components/lobby/LobbyManager.tsx +++ b/src/components/lobby/LobbyManager.tsx @@ -55,8 +55,8 @@ export default function LobbyManager({ roomId }: LobbyManagerProps) { } return ( -
-
+
+

Зал ожидания

{entries.length > 0 && ( @@ -68,7 +68,7 @@ export default function LobbyManager({ roomId }: LobbyManagerProps) { {entries.length === 0 ? (

Никто не ожидает

) : ( -
+
{entries.map((entry) => (
- prev.map((m) => (m.id === optimisticMsg.id ? { ...msg, createdAt: msg.createdAt } : m)) - ); + setMessages((prev) => { + // Check if SSE already delivered this message (race condition) + const sseAlreadyDelivered = prev.some((m) => m.id === msg.id); + if (sseAlreadyDelivered) { + // Just remove the optimistic message + return prev.filter((m) => m.id !== optimisticMsg.id); + } + // Replace optimistic with real + return prev.map((m) => (m.id === optimisticMsg.id ? msg : m)); + }); } } catch { // Remove optimistic message on failure diff --git a/src/components/room/ModerationPanel.tsx b/src/components/room/ModerationPanel.tsx index 70d75e4..2263218 100644 --- a/src/components/room/ModerationPanel.tsx +++ b/src/components/room/ModerationPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useCallback } from "react"; -import { useParticipants } from "@livekit/components-react"; +import { useParticipants, useLocalParticipant } from "@livekit/components-react"; import type { ModerationAction } from "@/types"; interface ModerationPanelProps { @@ -10,19 +10,26 @@ interface ModerationPanelProps { export default function ModerationPanel({ roomId }: ModerationPanelProps) { const participants = useParticipants(); + const { localParticipant } = useLocalParticipant(); const [acting, setActing] = useState(null); + const [error, setError] = useState(""); const moderate = useCallback( async (action: ModerationAction, targetSessionId?: string) => { + setError(""); setActing(targetSessionId ?? action); try { - await fetch(`/api/rooms/${roomId}/moderate`, { + const res = await fetch(`/api/rooms/${roomId}/moderate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, targetSessionId }), }); + if (!res.ok) { + const data = await res.json().catch(() => null); + setError(data?.error ?? `Ошибка: ${res.status}`); + } } catch { - // silent + setError("Ошибка сети"); } finally { setActing(null); } @@ -30,14 +37,22 @@ export default function ModerationPanel({ roomId }: ModerationPanelProps) { [roomId] ); + const myIdentity = localParticipant?.identity; + return ( -
-

Модерация

+
+

Модерация

+ + {error && ( +
+ {error} +
+ )} -
+

Участники ({participants.length})

- {participants.map((p) => ( -
-
-
- - {(p.name || p.identity).charAt(0).toUpperCase()} + {participants.map((p) => { + const isMe = p.identity === myIdentity; + return ( +
+
+
+ + {(p.name || p.identity).charAt(0).toUpperCase()} + +
+ + {p.name || p.identity} + {isMe && (вы)}
- - {p.name || p.identity} - + {!isMe && ( +
+ + +
+ )}
-
- - -
-
- ))} + ); + })}