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:
2026-06-15 21:38:28 +08:00
commit f21fa5b5e8
239 changed files with 29010 additions and 0 deletions
@@ -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>
);
}