import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import {
ArrowLeft,
AtSign,
Bell,
CheckCheck,
Flag,
ImagePlus,
Inbox,
Mail,
MessageCircleReply,
Search,
Send,
ShieldAlert,
Smile,
X,
XCircle,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { notify } from '@/lib/notify';
import { api } from '../api/client';
import type { MessageConversation, PrivateMessage, User } from '../api/types';
import { useAuth } from '../hooks/useAuth';
import { loginPath } from '../utils/authRedirect';
import { useNoIndexSEO } from '../hooks/usePageSEO';
import { formatConvListTime, formatDateTime, formatTime } from '../utils/content';
import { postPath } from '../utils/permalink';
import { userPath } from '../utils/userPath';
import { InFlowSiteFooter } from '../components/SiteFooter';
import StickerPicker from '../components/emoji/StickerPicker';
import type { Sticker } from '../data/stickers';
import PmComposerInput, { isPmStickerSrc, type PmComposerInputHandle } from '../components/PmComposerInput';
import { ArticleImagePickerDialog } from '../components/editor/ArticleImagePickerDialog';
import { Tooltip } from '../components/ui/Tooltip';
import { cn } from '@/lib/utils';
import { getSessionSnapshot, setSessionSnapshot, deleteSessionSnapshot } from '../utils/sessionPageCache';
import { PAGE_FORCE_REFRESH_EVENT } from '../utils/feedCache';
import { PAGE_SOFT_REFRESH_COMMIT_EVENT } from '../utils/softRefresh';
type MsgTab = 'dm' | 'notify';
type ConvSnap = { conversations: MessageConversation[]; total: number; page: number };
type ThreadSnap = { messages: PrivateMessage[]; total: number; peerUser: User | null };
type PmDraftEmbed = { id: string; url: string; name: string };
type PmDraft = { text: string; embeds: PmDraftEmbed[] };
function newEmbedId() {
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
function serializePmDraft(text: string, embeds: PmDraftEmbed[]) {
const trimmed = text.trim();
const parts = [
...(trimmed ? [trimmed] : []),
...embeds.map((e) => ``),
];
return parts.join('\n');
}
function isDraftEmpty(text: string, embeds: PmDraftEmbed[]) {
return !text.trim() && embeds.length === 0;
}
/** 搜索/主页跳转用的对方用户摘要 → User */
function toPeerUser(u: { id: number; username: string; nickname: string; avatar?: string }): User {
return {
id: u.id,
username: u.username,
nickname: u.nickname,
avatar: u.avatar || '',
role: 'user',
};
}
function makePendingConversation(peer: User): MessageConversation {
return {
peer_user_id: peer.id,
peer_user: peer,
is_system: false,
unread_count: 0,
updated_at: new Date().toISOString(),
};
}
const NOTIFY_KINDS = [
{ key: 'all', label: '全部' },
{ key: 'reply', label: '回复' },
{ key: 'mention', label: '@提及' },
{ key: 'moderation', label: '待审' },
{ key: 'reject', label: '未通过' },
{ key: 'report_result', label: '举报' },
{ key: 'system', label: '系统' },
] as const;
function kindLabel(kind: string, relatedStatus?: string) {
if (kind === 'moderation') {
if (relatedStatus && relatedStatus !== 'pending') return '审核';
return '待审';
}
switch (kind) {
case 'reject': return '未通过';
case 'report_result': return '举报结果';
case 'reply': return '回复提醒';
case 'mention': return '@提及';
case 'system': return '系统通知';
default: return '通知';
}
}
function statusLabel(status?: string) {
switch (status) {
case 'pending': return '待审';
case 'published': return '已通过';
case 'rejected': return '未通过';
case 'deleted': return '已删除';
default: return '';
}
}
/** 仍待处理的审核通知(无状态按待审处理,兼容未回填) */
function isPendingModeration(m: PrivateMessage) {
return m.kind === 'moderation' && (!m.related_status || m.related_status === 'pending');
}
/** 私信气泡:纯文本 + 简易 markdown 图片 `` */
function PmBubbleContent({ content }: { content: string }) {
const parts = content.split(/(!\[[^\]]*]\([^)]+\))/g);
return (
{parts.map((part, i) => {
const m = part.match(/^!\[([^\]]*)]\(([^)]+)\)$/);
if (m) {
const sticker = isPmStickerSrc(m[2]);
return (
![{m[1]]({m[2]})
);
}
return part ?
{part} : null;
})}
);
}
function KindIcon({ kind }: { kind: string }) {
const props = { size: 15, 'aria-hidden': true as const };
switch (kind) {
case 'reply': return ;
case 'mention': return ;
case 'moderation': return ;
case 'reject': return ;
case 'report_result': return ;
default: return ;
}
}
/** 按通知类型生成跳转目标与 CTA 文案(待审跳前台帖/楼层) */
function notifyTarget(m: PrivateMessage): { to: string; label: string } | null {
const postID = m.related_post_id;
const commentID = m.related_comment_id;
const floor = m.related_floor && m.related_floor > 0 ? m.related_floor : 0;
const looksLikeComment =
(m.subject || '').includes('评论') || (m.content || '').includes('评论');
if (m.kind === 'moderation') {
if (!postID) return null;
const isComment = !!commentID || looksLikeComment;
const path = isComment && floor > 0
? `${postPath(postID)}#floor-${floor}`
: postPath(postID);
const pending = isPendingModeration(m);
if (isComment) {
return { to: path, label: pending ? '去审核评论' : '查看评论' };
}
return { to: path, label: pending ? '去审核帖子' : '查看帖子' };
}
if (postID) {
const path = floor > 0 ? `${postPath(postID)}#floor-${floor}` : postPath(postID);
if (m.kind === 'reply' || m.kind === 'mention') {
return { to: path, label: floor > 0 ? '查看回复' : '查看帖子' };
}
if (m.kind === 'reject' && (commentID || looksLikeComment)) {
return { to: path, label: '查看评论' };
}
if (m.kind === 'report_result' && floor > 0) {
return { to: path, label: '查看评论' };
}
return { to: path, label: '查看帖子' };
}
return null;
}
function peerTitle(conv: MessageConversation | null, peerUser: User | null | undefined, peerId: number) {
if (peerId === 0 || conv?.is_system) return '系统通知';
return peerUser?.nickname || conv?.peer_user?.nickname || `用户 #${peerId}`;
}
function peerInitial(name: string) {
return name.trim().charAt(0) || '?';
}
function previewText(msg?: PrivateMessage) {
if (!msg) return '暂无消息';
const raw = (msg.content || msg.subject || '').trim();
if (!raw) return msg.subject || '暂无消息';
const text = raw
.replace(/!\[[^\]]*]\([^)]+\)/g, '[图片]')
.replace(/\s+/g, ' ')
.trim();
return text || msg.subject || '暂无消息';
}
function AvatarBubble({
name,
avatar,
system,
className,
}: {
name: string;
avatar?: string;
system?: boolean;
className?: string;
}) {
if (system) {
return (
);
}
if (avatar) {
return
;
}
return {peerInitial(name)};
}
function parseTab(raw: string | null, peer: string | null): MsgTab {
if (peer !== null && peer !== '') return 'dm';
return raw === 'notify' ? 'notify' : 'dm';
}
export default function MessagesPage() {
const nav = useNavigate();
const { user, loading: authLoading } = useAuth();
const [params, setParams] = useSearchParams();
useNoIndexSEO('站内消息');
const peerParam = params.get('peer');
const tab = parseTab(params.get('tab'), peerParam);
const selectedPeer = peerParam === null || peerParam === ''
? null
: Number(peerParam);
const peerSelected = tab === 'dm' && selectedPeer !== null && !Number.isNaN(selectedPeer);
const notifyKind = params.get('kind') || 'all';
const [conversations, setConversations] = useState([]);
const [convTotal, setConvTotal] = useState(0);
const [convPage, setConvPage] = useState(1);
const [listLoading, setListLoading] = useState(true);
const [messages, setMessages] = useState([]);
const [msgTotal, setMsgTotal] = useState(0);
const [threadLoading, setThreadLoading] = useState(false);
const [loadingOlder, setLoadingOlder] = useState(false);
const [peerUser, setPeerUser] = useState(null);
const [draftText, setDraftText] = useState('');
const [draftEmbeds, setDraftEmbeds] = useState([]);
const [showSticker, setShowSticker] = useState(false);
const [imagePickerOpen, setImagePickerOpen] = useState(false);
const composerRef = useRef(null);
const draftsByPeerRef = useRef