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 && ( +
+ + +
+ )}
-
- - -
-
- ))} + ); + })}