feat(web): add account workspaces and signed content flows

This commit is contained in:
justme committed 2026-10-14 04:06:03 +08:00
1 parent 7b8d61d1ba
commit b5cf3d5958
117 files changed
+2789 -1674

No files matched your search

+10 -7
View File
@@ -8,28 +8,31 @@ import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import styles from './ChatPane.module.css';
export function ChatMessage({ id, onOpenFile, onDelete }: { id: string; onOpenFile: (id: string) => void; onDelete: (id: string) => void }) {
export function ChatMessage({ id, compact = false, onOpenFile, onDelete }: { id: string; compact?: boolean; onOpenFile: (id: string) => void; onDelete: (id: string) => void }) {
const item = useWorkspace((s) => s.byId[id]);
const owner = useWorkspace((s) => s.channel?.role === 'owner');
const ownDevice = useWorkspace((s) => s.devices.find((device) => device.self)?.name);
const username = useAccount((s) => s.username);
const download = useDownload();
const [downloading, setDownloading] = useState(false);
if (!item) return null;
const sender = item.author;
const name = sender?.displayName || sender?.username || item.deviceId || '未知用户';
const self = sender?.username !== undefined && sender.username === username;
const self = sender?.username !== undefined ? sender.username === username : Boolean(ownDevice && item.deviceId === ownDevice);
const ready = item.state.kind === 'complete';
async function save() {
if (!item || downloading) return;
setDownloading(true); try { await download(item); } finally { setDownloading(false); }
}
return <article className={`${styles.message} ${self ? styles.mine : ''}`} aria-label={item.kind === 'file' ? `${name} 上传了 ${item.name}` : `${name} 的消息`}>
{sender?.avatar ? <img className={styles.avatar} src={sender.avatar} alt={`${name} 的头像`} />
return <article className={`${styles.message} ${self ? styles.mine : ''} ${compact ? styles.compact : ''}`} title={compact ? `${name} · ${formatTime(item.ts)}` : undefined}
aria-label={item.kind === 'file' ? `${name} 上传了 ${item.name}` : `${name} 的消息`}>
{compact ? <span className={styles.avatarSpacer} aria-hidden="true" /> : sender?.avatar ? <img className={styles.avatar} src={sender.avatar} alt={`${name} 的头像`} />
: <span className={`${styles.avatar} ${styles.avatarFallback}`} role="img" aria-label={`${name} 的默认头像`}>{Array.from(name)[0] ?? '?'}</span>}
<div className={styles.bubble}>
<header><strong className={styles.senderName} title={sender ? `${sender.username} · ${item.deviceId}` : item.deviceId}>{name}</strong><time>{formatTime(item.ts)}</time>
{owner || self ? <Button variant="ghost" icon="trash" label={item.kind === 'file' ? `删除文件消息 ${item.name}` : '删除消息'} onClick={() => onDelete(id)} /> : null}
</header>
{!compact ? <header><strong className={styles.senderName} title={sender ? `${sender.username} · ${item.deviceId}` : item.deviceId}>{name}</strong><time>{formatTime(item.ts)}</time>
{owner || self ? <Button className={styles.deleteAction} variant="ghost" icon="trash" label={item.kind === 'file' ? `删除文件消息 ${item.name}` : '删除消息'} onClick={() => onDelete(id)} /> : null}
</header> : null}
{compact && (owner || self) ? <Button className={`${styles.deleteAction} ${styles.compactDelete}`} variant="ghost" icon="trash" label={item.kind === 'file' ? `删除文件消息 ${item.name}` : '删除消息'} onClick={() => onDelete(id)} /> : null}
{item.state.kind === 'tampered' ? <p className={styles.integrity}>内容校验失败,已拒绝显示。</p> : item.kind === 'text' ? <p className={styles.messageText}>{item.text}</p>
: <div className={styles.fileMessage}>
<button type="button" className={styles.fileCard} aria-label={`预览文件 ${item.name}`} disabled={!ready} onClick={() => onOpenFile(id)}>