c79b176b87
- P2P 接收端(桌面)改用混合有界内存 sink:wails:// 自定义 scheme 无持久 WebView 存储致 OPFS 降级、createWritable 回压不畅,把接收链堵死、饿死 dc.onmessage → 接收方 rwnd=0 → 发送方 bufferedAmount 卡死 16MiB(PWA→Mac 约 15MB 死锁)。小文件(暂存 < 256MB)纯内存、 close 整文件过 Go 写盘;超上限大文件流式 spill 到 Go 临时文件(每 64MB 一批),内存恒定 有界。浏览器 / iOS 接收仍走 OPFS - 新增桌面流式落盘:download.go 的 Begin/Append/Finalize/AbortStreamingDownload(复用 SaveDownload 的目录解析 / 文件名 sanitize / 不可写回退 / reveal,临时文件同目录便于原子 rename),app.go 加 4 个绑定 + net/desktop.ts 封装;含单测 download_stream_test.go; revealInFileManager 加 CDROP_NO_REVEAL 守卫(headless / 测试不弹 Finder) - IncomingSink.close() 返回 SinkResult(blob | 已落盘 path),新增 deliverIncoming 统一 落地;relay 接收一并改用,桌面大文件经中继也有界落盘 - P2P 发送端进度改按“已交付字节”(bytesSent − bufferedAmount)计,而非已塞进本地 16MiB buffer 的量——后者在 buffer 秒满后定格、误报“疑似卡住”;并加 250ms 轮询在 backpressure 等待期持续刷新进度 - P2P 发送端送完后等 bufferedAmount 抽干到 0 再宣告 completed(原先入 buffer 即标完成, 与接收端仍在下载的状态不一致) - 前端动态 CJK 文本累积修复:聚珍 shim 把就地更新的 CJK 文本节点 charify 拆段后,旧碎片 残留累积(典型“发送到 X”切设备多出“到”)。新增 DynText(key=文本内容触发整体重挂、 保留中西排版),应用于发送按钮 / 设备名 / 在线数 / 相对时间 / 会话与令牌活跃时间 / 消息 计数 / 剪贴板来源 / 问候语等就地更新处 - 传输卡片新增实时速度:store upsertTransfer 据相邻样本差分 + EMA 平滑算 bytesPerSec, TransferRow 在字节流动且未卡死时以“X/秒”展示(点分隔 meta 行追加)
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { X } from "lucide-react";
|
|
import { Button, DynText } 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}>
|
|
<DynText>{t("home.message.countSuffix", { count: messages.length })}</DynText>
|
|
</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} · <DynText>{formatRelative(m.sentAt)}</DynText></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>
|
|
);
|
|
}
|