"use client" import { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" import { toast } from "sonner" import { useTranslations } from "next-intl" import { ArrowLeft, Loader2, Mail, Reply, RotateCcw, Star, Trash2 } from "lucide-react" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/shared/components/ui/dialog" import { ConfirmDeleteDialog } from "@/shared/components/ui/confirm-delete-dialog" import { cn, formatDate } from "@/shared/lib/utils" import { usePermission } from "@/shared/hooks/use-permission" import { Permissions } from "@/shared/types/permissions" import { deleteMessageAction, getMessageThreadAction, recallMessageAction, toggleMessageStarAction } from "../actions" import { buildReplyHref } from "../lib/build-reply-href" import { MessageReportBlock } from "./message-report-block" import type { Message } from "../types" export function MessageDetail({ message, currentUserId, backHref = "/messages", }: { message: Message currentUserId: string backHref?: string }) { const t = useTranslations("messages") const router = useRouter() const [isWorking, setIsWorking] = useState(false) const [deleteOpen, setDeleteOpen] = useState(false) const [recallOpen, setRecallOpen] = useState(false) const [isRecalling, setIsRecalling] = useState(false) const [isStarred, setIsStarred] = useState(message.isStarred) const [isTogglingStar, setIsTogglingStar] = useState(false) // P1-3: 线程状态 const [thread, setThread] = useState([]) const [loadingThread, setLoadingThread] = useState(true) // P2-4: 撤回状态(同步 UI,撤回成功后立即显示占位) const [isRecalled, setIsRecalled] = useState(message.recalledAt !== null) const { hasPermission } = usePermission() const canSend = hasPermission(Permissions.MESSAGE_SEND) const canDelete = hasPermission(Permissions.MESSAGE_DELETE) const isReceived = message.receiverId === currentUserId // P2-4: 仅发送方在 2 分钟窗口内、且消息未被撤回时可撤回 const canRecall = !isReceived && canSend && !isRecalled && Date.now() - new Date(message.createdAt).getTime() <= 2 * 60 * 1000 const counterpart = isReceived ? message.senderName : message.receiverName const counterpartLabel = isReceived ? t("meta.from") : t("meta.to") // P1-3: 获取消息线程(根消息 + 回复链) useEffect(() => { let active = true setLoadingThread(true) void getMessageThreadAction(message.id) .then((res) => { if (!active) return if (res.success && res.data) { setThread(res.data) } else { setThread([]) } }) .catch(() => { if (!active) return setThread([]) }) .finally(() => { if (active) setLoadingThread(false) }) return () => { active = false } }, [message.id]) const handleDelete = async () => { setIsWorking(true) try { const res = await deleteMessageAction(message.id) if (res.success) { toast.success(res.message) router.push("/messages") router.refresh() } else { toast.error(res.message || t("messages.deleteFailed")) } } catch { toast.error(t("messages.deleteFailed")) } finally { setIsWorking(false) setDeleteOpen(false) } } const handleToggleStar = async () => { setIsTogglingStar(true) const prevStarred = isStarred // 乐观更新 setIsStarred(!prevStarred) try { const res = await toggleMessageStarAction(message.id) if (res.success) { toast.success(t("messages.starToggled")) } else { // 回滚 setIsStarred(prevStarred) toast.error(res.message) } } catch { // 回滚 setIsStarred(prevStarred) toast.error(t("messages.sendFailed")) } finally { setIsTogglingStar(false) } } // P2-4: 消息撤回(仅发送方,2 分钟窗口内) const handleRecall = async () => { setIsRecalling(true) try { const res = await recallMessageAction(message.id) if (res.success) { // 同步 UI:立即显示"已撤回"占位 setIsRecalled(true) toast.success(t("messages.recalled")) router.refresh() } else { // 失败:根据返回消息判断是否超时 if (res.message === "Recall window expired") { toast.error(t("messages.recallExpired")) } else { toast.error(res.message || t("messages.recallFailed")) } } } catch { toast.error(t("messages.recallFailed")) } finally { setIsRecalling(false) setRecallOpen(false) } } // P1-7: 使用纯函数构建回复链接(替代组件内字符串拼接) const replyHref = buildReplyHref(message, currentUserId, canSend) // 线程展示:根消息已渲染在主区域,这里仅展示回复(时间正序) // data-access 返回倒序(最新在前),UI 反转为正序(最早回复在前) const replies = thread .filter((m) => m.id !== message.id) .slice() .reverse() return (

{t("title.detail")}

{/* P2-4: 撤回按钮(仅发送方可见且未撤回、未超时) */} {canRecall ? ( ) : null} {canSend && !isRecalled ? ( ) : null} {canDelete ? ( ) : null} {/* P2-5: 举报 + 屏蔽(仅收到的消息,非自己发送的) */} {isReceived && !isRecalled ? ( ) : null}
{message.subject ?? t("meta.noSubject")}
{counterpartLabel}: {counterpart ?? t("meta.unknown")} · {formatDate(message.createdAt)} {message.readAt && isReceived ? ( <> · {t("meta.readAt", { date: formatDate(message.readAt) })} ) : null}
{/* P2-4: 已撤回消息显示占位文案,隐藏原始内容 */} {isRecalled ? (

{t("status.recalled")}

) : (

{message.content}

)}
{/* P1-3: 消息线程(回复链) */} {t("thread.title")} {loadingThread ? ( {loadingThread ? null : replies.length === 0 ? (

{t("thread.noReplies")}

) : ( replies.map((reply) => { const replyIsReceived = reply.receiverId === currentUserId const replyCounterpart = replyIsReceived ? reply.senderName : reply.receiverName const replyIsSelf = reply.senderId === currentUserId const replyRecalled = reply.recalledAt !== null return (
{replyIsSelf ? t("thread.you") : (replyCounterpart ?? t("meta.unknown"))} {formatDate(reply.createdAt)}
{/* P2-4: 线程中已撤回回复显示占位文案 */} {replyRecalled ? (

{t("status.recalled")}

) : (

{reply.content}

)}
) }) )}
{/* P2-4: 撤回确认对话框 */} {t("actions.recall")} {t("messages.recallConfirm")}
) }