import { useEffect, useState } from 'react'; import { useMailStore } from '../stores/mailStore'; import { useSessionStore } from '../stores/sessionStore'; import { useUIStore } from '../stores/uiStore'; import type { Mail } from '../types'; import { groupMailsBySession, isFlatGroup, splitByPermission, type MailGroup } from '../lib/mailGroups'; import { ShieldIcon, PaperclipIcon, ChevronRightIcon } from './icons'; import { participantAddress } from '../lib/replyTarget'; export default function MailList() { const viewMode = useUIStore(s => s.viewMode); const cancelCompose = useUIStore(s => s.cancelCompose); const showDetail = useUIStore(s => s.showDetail); const inbox = useMailStore(s => s.inbox); const sent = useMailStore(s => s.sent); const currentMail = useMailStore(s => s.currentMail); const selectMail = useMailStore(s => s.selectMail); const fetchInbox = useMailStore(s => s.fetchInbox); const fetchSent = useMailStore(s => s.fetchSent); const clearSession = useSessionStore(s => s.clearSession); // 哪些会话组被展开。默认全部折叠 —— 收件箱的问题正是「一次任务的几十封信 // 淹掉其他任务」,默认展开等于没分组 const [expanded, setExpanded] = useState>(new Set()); useEffect(() => { if (viewMode === 'sent') fetchSent(); else if (viewMode === 'inbox') fetchInbox('all'); }, [viewMode]); // 切换收件箱/发件箱时收起所有组:两个箱子的会话集合不同, // 留着上一个箱子的展开状态会让人以为某个组「自己展开了」 useEffect(() => { setExpanded(new Set()); }, [viewMode]); const isSent = viewMode === 'sent'; // 权限请求已经有自己的导航项(授权),收件箱只放要读的内容。 // 不过滤的后果实测过:一个会话的 17 封权限邮件把另外两个会话的信挤出视野。 // 发件箱不筛:人发不出权限请求(那是 Agent 发的),筛也筛不掉什么。 const source = isSent ? sent : splitByPermission(inbox).normal; const list = source; const groups = groupMailsBySession(list); const toggle = (sessionId: string) => { setExpanded(prev => { const next = new Set(prev); if (next.has(sessionId)) next.delete(sessionId); else next.add(sessionId); return next; }); }; const pick = (m: Mail) => { clearSession(); cancelCompose(); selectMail(m); // 窄屏下列表与详情共用一栏,选中后要切过去; // 宽屏下这个状态不影响渲染(两栏并排),但仍然维护 —— // 否则从窄屏拖宽再拖回来,用户会发现自己回到了列表,刚打开的邮件不见了 showDetail(); }; return (

{isSent ? '发件箱' : '收件箱'}

{/* 显示「会话数 · 邮件数」而不是只显示邮件数:分组之后前者才是 「有几件事」,后者只是流量 */} {groups.length > 0 && groups.length !== list.length ? `${groups.length} 组 · ${list.length} 封` : list.length}
{groups.map(g => isFlatGroup(g) ? ( pick(g.latest)} /> ) : ( toggle(g.sessionId)} currentMailID={currentMail?.mail_id} onPick={pick} /> ) )} {list.length === 0 && (

暂无邮件

)}
); } /** * 一个会话折叠成的一组。 * * 组头答的是「哪件事、进行到哪」;展开后才是逐封邮件。 * 权限请求不在这里 —— 它们在单独的「授权」导航项里。 */ function SessionGroup({ group, showTo, open, onToggle, currentMailID, onPick }: { group: MailGroup; showTo: boolean; open: boolean; onToggle: () => void; currentMailID?: string; onPick: (m: Mail) => void; }) { const g = group; const time = new Date(g.latest.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); // 列表行只显示「跟谁在通信」,**不带会话位**:分组头下面已经单独显示了 // 会话别名,再拼一遍会让长别名(实测 92 字节)把这一行挤没。 // // 人还是 Agent 走显式布尔;workspace 从会话取(from_workspace 对 Agent // 存的是 Agent 名而非路径)。 const isPeerHuman = showTo ? g.latest.to_human : g.latest.from_human; const peer = participantAddress( showTo ? g.latest.to_name : g.latest.from_name, isPeerHuman, isPeerHuman ? '' : g.latest.session_workspace || '' ); // 组内含选中邮件时给个边框,否则展开一个组再滚下去会找不到自己在看哪封 const hasActive = currentMailID ? g.mails.some(m => m.mail_id === currentMailID) : false; return (
{open && (
{g.mails.map(m => ( onPick(m)} compact /> ))}
)}
); } function MailItem({ mail, active, showTo, onClick, compact = false }: { mail: Mail; active: boolean; showTo: boolean; onClick: () => void; /** 组内条目:对端信息已在组头显示,这里省掉以免每行都重复同一个地址 */ compact?: boolean; }) { const isPermission = mail.mail_type === 'permission_request'; const isUnread = mail.status === 'unread'; const ccCount = mail.cc_list?.length ?? 0; const attachCount = mail.attachments?.length ?? 0; const time = new Date(mail.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); const isRowHuman = showTo ? mail.to_human : mail.from_human; const peer = participantAddress( showTo ? mail.to_name : mail.from_name, isRowHuman, isRowHuman ? '' : mail.session_workspace || '' ); return ( ); }