cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "../../ui/primitives";
|
||||
import type { MessageRecord } from "../../store";
|
||||
import { t } from "../../i18n";
|
||||
import { formatRelative } from "../../utils/format";
|
||||
import s from "./MessageList.module.css";
|
||||
|
||||
export interface MessageListProps
|
||||
{
|
||||
messages: MessageRecord[];
|
||||
onDismiss: (id: string) => void;
|
||||
onClearAll: () => void;
|
||||
}
|
||||
|
||||
export function MessageList(props: MessageListProps)
|
||||
{
|
||||
const { messages, onDismiss, onClearAll } = props;
|
||||
if (messages.length === 0) { return null; }
|
||||
|
||||
return (
|
||||
<section className={s.section}>
|
||||
<div className={s.head}>
|
||||
<div>
|
||||
<span className={s.title}>{t("home.message.title")}</span>
|
||||
<span className={s.count}>
|
||||
{t("home.message.countSuffix", { count: messages.length })}
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={onClearAll}>
|
||||
{t("home.message.clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={s.list}>
|
||||
{messages.map((m) => (
|
||||
<MessageRow key={m.id} m={m} onDismiss={() => onDismiss(m.id)} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageRow(props: { m: MessageRecord; onDismiss: () => void })
|
||||
{
|
||||
const { m, onDismiss } = props;
|
||||
const arrow = m.direction === "outgoing" ? "→" : "←";
|
||||
const cls = [ s.row, m.direction === "incoming" ? s.rowIncoming : "" ].join(" ");
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
<div className={s.body}>
|
||||
<div className={s.meta}>{arrow} {m.peerName} · {formatRelative(m.sentAt)}</div>
|
||||
<div className={`${s.text} justify`} data-jz-level="paragraph">{m.text}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={s.dismiss}
|
||||
aria-label={t("common.dismiss")}
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user