fix and update

This commit is contained in:
lendry
2026-07-01 19:19:27 +03:00
parent f423f512f8
commit 7e54cec361
16 changed files with 1093 additions and 150 deletions

View File

@@ -0,0 +1,273 @@
'use client';
import type { ReactNode } from 'react';
import { Loader2, Trash2 } from 'lucide-react';
import { ChatMediaAlbumGrid } from '@/components/chat/chat-media-album-grid';
import { MediaImageSkeleton } from '@/components/ui/media-image-skeleton';
import { AdminChatMessage, ChatMessage } from '@/lib/api';
import { groupChatMessagesForDisplay, parseChatMediaMetadata } from '@/lib/chat-media-album';
import { cn } from '@/lib/utils';
import { AdminChatMediaEntry, adminMessageToChatMessage } from '@/components/admin/use-admin-chat-media';
function AdminImageBubble({
message,
media,
onOpen
}: {
message: ChatMessage;
media?: AdminChatMediaEntry;
onOpen: () => void;
}) {
const meta = parseChatMediaMetadata(message.metadataJson);
const fileName = meta.fileName || message.content || 'Фото';
if (media?.status === 'error') {
return <p className="text-sm text-[#667085]">{media.errorMessage ?? 'Фото недоступно'}</p>;
}
if (!media?.blobUrl || media.status === 'loading') {
return <MediaImageSkeleton className="max-h-56 w-full max-w-[240px]" rounded="xl" />;
}
return (
<button type="button" className="block overflow-hidden rounded-xl" onClick={onOpen}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={media.blobUrl} alt={fileName} className="max-h-56 max-w-full rounded-xl object-cover" />
</button>
);
}
export function AdminInspectorMessageBody({
messages,
roomId,
mediaUrls,
onOpenImage
}: {
messages: AdminChatMessage[];
roomId: string;
mediaUrls: Record<string, AdminChatMediaEntry>;
onOpenImage: (message: ChatMessage) => void;
}) {
const chatMessages = messages.map((message) => adminMessageToChatMessage(message, roomId));
const displayItems = groupChatMessagesForDisplay(chatMessages);
return (
<div className="space-y-2">
{displayItems.map((item) => {
if (item.kind === 'album') {
const captionMessage = item.messages.find((message) => message.content?.trim());
return (
<div key={item.albumId} className="space-y-2">
<ChatMediaAlbumGrid
messages={item.messages}
mediaUrls={mediaUrls}
onImageClick={(message) => onOpenImage(message)}
/>
{captionMessage?.content ? (
<p className="whitespace-pre-wrap break-words text-sm text-[#344054]">{captionMessage.content}</p>
) : null}
</div>
);
}
const message = item.message;
const adminMessage = messages.find((entry) => entry.id === message.id);
if (!adminMessage) return null;
if (message.isEncrypted) {
return (
<p key={message.id} className="text-sm text-[#667085]">
[E2E недоступно для модерации]
</p>
);
}
if (message.storageKey && message.type === 'IMAGE') {
return (
<AdminImageBubble
key={message.id}
message={message}
media={message.storageKey ? mediaUrls[message.storageKey] : undefined}
onOpen={() => onOpenImage(message)}
/>
);
}
if (message.type === 'FILE' && message.storageKey) {
const meta = parseChatMediaMetadata(message.metadataJson);
return (
<p key={message.id} className="text-sm text-[#667085]">
📎 {meta.fileName || message.content || 'Файл'}
</p>
);
}
if (message.type === 'VOICE' || message.type === 'AUDIO') {
return (
<p key={message.id} className="text-sm text-[#667085]">
🎤 Голосовое сообщение
</p>
);
}
if (message.type === 'POLL') {
return (
<p key={message.id} className="text-sm text-[#667085]">
📊 Опрос
</p>
);
}
const text = adminMessage.content?.trim() || adminMessage.preview;
if (!text || text === '[Пустое сообщение]') {
return null;
}
return (
<p
key={message.id}
className={cn(
'whitespace-pre-wrap break-words text-sm text-[#344054]',
message.type === 'SYSTEM' && 'text-[#667085] italic'
)}
>
{text}
</p>
);
})}
</div>
);
}
export function AdminInspectorMessageFeed({
messages,
roomId,
mediaUrls,
deletingMessageId,
onDelete,
onOpenImage
}: {
messages: AdminChatMessage[];
roomId: string;
mediaUrls: Record<string, AdminChatMediaEntry>;
deletingMessageId: string | null;
onDelete: (messageId: string) => void;
onOpenImage: (message: ChatMessage) => void;
}) {
const chatMessages = messages.map((message) => adminMessageToChatMessage(message, roomId));
const displayItems = groupChatMessagesForDisplay(chatMessages);
const adminById = new Map(messages.map((message) => [message.id, message]));
return (
<div className="space-y-2">
{displayItems.map((item) => {
if (item.kind === 'album') {
const anchor = adminById.get(item.messages[0]!.id);
if (!anchor) return null;
return (
<AdminInspectorMessageRow
key={item.albumId}
message={anchor}
roomId={roomId}
mediaUrls={mediaUrls}
deleting={item.messages.some((message) => deletingMessageId === message.id)}
onDelete={() => onDelete(anchor.id)}
onOpenImage={onOpenImage}
bodyOverride={
<AdminInspectorMessageBody
messages={item.messages
.map((message) => adminById.get(message.id))
.filter((entry): entry is AdminChatMessage => Boolean(entry))}
roomId={roomId}
mediaUrls={mediaUrls}
onOpenImage={onOpenImage}
/>
}
/>
);
}
const adminMessage = adminById.get(item.message.id);
if (!adminMessage) return null;
return (
<AdminInspectorMessageRow
key={item.message.id}
message={adminMessage}
roomId={roomId}
mediaUrls={mediaUrls}
deleting={deletingMessageId === adminMessage.id}
onDelete={() => onDelete(adminMessage.id)}
onOpenImage={onOpenImage}
/>
);
})}
</div>
);
}
export function AdminInspectorMessageRow({
message,
roomId,
mediaUrls,
deleting,
onDelete,
onOpenImage,
bodyOverride
}: {
message: AdminChatMessage;
roomId: string;
mediaUrls: Record<string, AdminChatMediaEntry>;
deleting: boolean;
onDelete: () => void;
onOpenImage: (message: ChatMessage) => void;
bodyOverride?: ReactNode;
}) {
return (
<div
className={cn(
'rounded-2xl border px-3 py-2',
message.isDeleted ? 'border-red-100 bg-red-50' : 'border-[#eceef4] bg-[#fafbff]'
)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">{message.senderName}</span>
<span className="text-xs text-[#667085]">
{new Intl.DateTimeFormat('ru-RU', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(message.createdAt))}
</span>
{message.isDeleted ? <span className="text-xs font-medium text-red-600">Удалено</span> : null}
</div>
<div className="mt-1">
{bodyOverride ?? (
<AdminInspectorMessageBody
messages={[message]}
roomId={roomId}
mediaUrls={mediaUrls}
onOpenImage={onOpenImage}
/>
)}
</div>
</div>
{!message.isDeleted && !message.isEncrypted ? (
<button
type="button"
className="shrink-0 rounded-lg p-2 text-red-600 transition hover:bg-red-50 disabled:opacity-60"
disabled={deleting}
aria-label="Удалить сообщение"
onClick={onDelete}
>
{deleting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
</button>
) : null}
</div>
</div>
);
}