feat(web): discord-style chat, members pane, invitations and optimistic send [CI]
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful

- chat: left-aligned discord-style messages with grouped senders, hover
  actions, an auto-growing composer and file cards
- send: render messages and files locally first and replace the
  placeholder when the worker publishes the real item; failures keep the
  text with a retry action
- navigation: space rail plus a flatter channel list with the space name
  header and collapsible groups
- members: right pane tabs (files / members) grouped by online, offline
  and pending users, with a prominent invite action and a top bar shortcut
- notifications: toast host for invite results; invitees get a toast with
  an accept action, and the invitation payload now carries the space name
  and inviter inside the encrypted grant
- users over devices: drop device names, the device list and device
  rename/release; senders show the account (guests show as guest) and the
  share panel only keeps links
- follow contract 7.0.0 (max_members, members_used, members_full, /join)
- docs: ui-design and frontend updated for the new layout and concepts
This commit is contained in:
justme committed 2026-10-14 22:19:25 +08:00
1 parent 5fd4ddd411
commit 4b8d0b5a5f
83 files changed
+1291 -661

No files matched your search

+3 -3
View File
@@ -101,7 +101,7 @@ type ItemState =
| 降级防御 | 链接含 `k` 就只接受 enc 且认证通过的帧 |
| 不把密钥、令牌、明文写入 `console`、错误上报、URL、`localStorage` | 「我的频道」只存 IndexedDB |
| 便捷分享草稿预览 | 原始文件只缓存在创建者本机 IndexedDB,不会随文件帧上传;移除条目或封存时清理,用于刷新后继续预览草稿 |
| 账号资源保险库 | Worker 派生登录凭据与 AES-GCM 密钥;同步资源访问凭据、邀请私钥和移除墓碑;不含个人资料、草稿内容、终端 S、设备名或 Blob。按账号/访客分库,会话通过本机 IndexedDB 恢复 |
| 账号资源保险库 | Worker 派生登录凭据与 AES-GCM 密钥;同步资源访问凭据、邀请私钥和移除墓碑;不含个人资料、草稿内容、访客终端 S 或 Blob。按账号/访客分库,会话通过本机 IndexedDB 恢复 |
| 频道与文件夹 | 后端持久化结构和资源归属,Worker 读取结构快照;加密文件元数据中的原始位置用于内容校验,UI 使用后端当前归属 |
| 条目预览在沙箱 iframe 渲染;raw 内容只当纯文本显示或下载 | 无内联渲染,不使用 `dangerouslySetInnerHTML` |
| 严格 CSP,所有脚本样式带 SRI,不使用内联脚本 | 与 architecture §5 一致 |
@@ -171,7 +171,7 @@ type ItemState =
## 12. 实际边界与会话
core-bridge/instance.ts 使用真实 Worker 实现。账号、设备和个人资料由 worker/account.ts 与 accountProfile.ts 编排;账号密钥包通过 accountChannels.ts 与 channelGrant.ts 领取和核对;空间成员/设置/设备席位通过 channelResources.ts 调用 API;频道、分组和目录由 channelThreads.ts 调用后端。workspaceRecords.ts 只读取加密文件元数据,不处理账户或结构操作。
core-bridge/instance.ts 使用真实 Worker 实现。账号、设备和个人资料由 worker/account.ts 与 accountProfile.ts 编排;账号密钥包通过 accountChannels.ts 与 channelGrant.ts 领取和核对;空间成员/设置/加入通过 channelResources.ts 调用 API;频道、分组和目录由 channelThreads.ts 调用后端。workspaceRecords.ts 只读取加密文件元数据,不处理账户或结构操作。
@webdrop/core 仅实现纯密码学与帧协议,不访问数据库或网络。sessions.ts 管本机已解锁会话。transfer.ts 管持久化密文发送队列,队列按资源/创建顺序索引,确保文件密文帧先于目录记录上传。downloadStorage.ts 优先使用 OPFS 逐块写入临时文件,认证结束后交付;不支持时最多 64 MiB 的 Blob 回退。
@@ -197,7 +197,7 @@ ContextMenu 是无业务 UI,负责 portal、视口限制、键盘焦点和关
ItemBase.author 由服务端资源作者和账号资料投影得到,晚到的解密内容不能覆盖服务器提供的作者与位置。客户端提供的作者声明不能覆盖服务端资料。这是服务端身份归属,不是端对端逐账号签名。
ChatPane 订阅当前频道的条目 ID,ChatMessage 单独订阅条目状态和上传进度;文件目录变成文件消息,不重复创建独立文本通知。预览/下载继续经已有 store action,移动文件不会重新上传。GATED 内容只有明确操作后才读取。
ChatPane 订阅当前频道的条目 ID(乐观发送:store 的 sendText/sendFiles 先写入 `local:` 占位项,worker 经 `localId` 带回后由 reduce 替换,失败可 `retrySend`),ChatMessage 单独订阅条目状态和上传进度;文件目录变成文件消息,不重复创建独立文本通知。预览/下载继续经已有 store action,移动文件不会重新上传。GATED 内容只有明确操作后才读取。
## 17. 本机凭据保存
+18 -6
View File
@@ -148,7 +148,7 @@ WebDrop 是**暂存区**,频道工作区:聊天按 thread 展示,文件按 thr
### 6.4.1 便捷分享
Share 是独立的只读发布资源,不创建频道成员、threads 或席位。创建者在草稿中添加内容和设置,明确确认后封存;服务端清除 owner_hash,前端丢弃管理密钥,此后只有读取入口。访客打开分享链接直接读取加密清单,下载受限文件时计次数,不经过频道加入流程。详见 protocol.md §15。
Share 是独立的只读发布资源,不创建频道成员或 threads。创建者在草稿中添加内容和设置,明确确认后封存;服务端清除 owner_hash,前端丢弃管理密钥,此后只有读取入口。访客打开分享链接直接读取加密清单,下载受限文件时计次数,不经过频道加入流程。详见 protocol.md §15。
### 6.5 频道 thread、文件管理与聊天
@@ -258,7 +258,7 @@ Share 是独立的只读发布资源,不创建频道成员、threads 或席位
| 码 | 页面 |
|---|---|
| `seats_full` | 加入预览页:「成员人数已满」,告知联系所有者 |
| `members_full` | 加入预览页:「成员人数已满」,告知联系所有者 |
| `not_joined`(被释放) | 频道页整体替换为「你已被移出此频道」 |
| `not_found` / `gone` | 「频道不存在或已过期」 |
| `rate_limited` | Toast,带重试倒计时 |
@@ -331,12 +331,20 @@ Share 是独立的只读发布资源,不创建频道成员、threads 或席位
## 13. 空间、频道与频道分组
界面层级统一为「空间 → 频道分组(可选)→ 频道 → 聊天与文件夹」。原顶层频道对用户称为空间,原 thread 对用户称为频道。新建空间自动创建“常规”频道。空间负责成员、邀请、成员人数上限和加密边界;账号内设备不重复计入人数,设备席位只用于单设备认证与撤销。频道分组只组织导航,不改变权限或加密密钥。
界面层级统一为「空间 → 频道分组(可选)→ 频道 → 聊天与文件夹」。原顶层频道对用户称为空间,原 thread 对用户称为频道。新建空间自动创建“常规”频道。空间负责成员、邀请、成员人数上限和加密边界;账号内设备不重复计入人数,设备只是账号的属性,只在账号设置里管理。频道分组只组织导航,不改变权限或加密密钥。
左侧上段显示账号与空间列表,下段按分组展示频道。支持新建/重命名分组、折叠分组、在分组内新建频道、将已有频道移入其他分组或移回未分组。旧 thread 自动作为未分组频道展示。中间显示当前频道聊天,右侧显示当前频道文件夹和文件。
桌面最左是 64px 的空间窄栏(每个空间一个首字方块,当前空间反色;下方是新建空间与加入空间),其右 240px 的频道列:顶部是空间名与加密状态(点击打开空间设置),下面按分组展示频道,分组标题小号灰字,悬停才显示 + 与 ⋯,频道行 32px、选中为整行淡底;便签并入频道列底部。账号卡片贴在左下角横跨两栏。手机上空间栏变成横向一排方块,空间名栏隐藏。支持新建/重命名分组、折叠分组、在分组内新建频道、将已有频道移入其他分组或移回未分组。旧 thread 自动作为未分组频道展示。中间显示当前频道聊天,右侧显示当前频道文件夹和文件。
空间设置集中管理成员、邀请和空间权限;频道管理提供名称与所属分组。分组名及归属由后端持久化并通过通知刷新,刷新与其他设备保持一致。协议/API 内部保留 channels/thread_id 等标识,避免因界面术语变更重建已有数据。
### 13.1 成员栏、邀请与通知
右侧栏顶部是「文件 / 成员 n」页签,默认文件;顶栏的人数按钮直接打开成员页,空间主还有一个 + 直接打开邀请弹窗。手机上「聊天 / 文件」切换里多一项「成员」。
成员页以用户为单位,按 Discord 的分组排列:在线 — n、离线 — n、待接受 — n(仅空间主可见,有「撤销」)。每行头像、昵称(有昵称时下方小字显示用户名),空间主带「空间主」标签;一个用户有多台设备时小字显示「在线设备/设备数 台设备在线」。**设备不再单独列出,只是用户的属性。**在线状态与设备数由后端按账号统计(`GET /channels/{id}/members` 的 `online`、`devices`、`online_devices`,在线 = 该账号在本频道有活动订阅),前端在成员页可见期间每 8 秒读取一次,不改动订阅流。空间主在页头有醒目的「+ 邀请成员」,成员行悬停出现「移除」(需确认)。邀请与成员管理不再放在空间设置里,设置里只留一句指引。
邀请弹窗只收用户名;成功、撤销、移除、失败都弹 toast。toast 在右下角,4 秒消失(带操作按钮的 10 秒),最多 4 条,成功 / 失败 / 提示用左侧色条区分,`aria-live`。被邀请方登录后每 8 秒轮询账号邀请,出现新邀请就弹「xxx 邀请你加入 yyy [接受]」,点「接受」直接加入并进入空间;账号设置里的邀请收件箱同样显示空间名与邀请人。空间名与邀请人放在邀请 payload 的加密内容里(`ChannelGrant.name / invitedBy`,均为可选字段),服务端不可见;旧邀请没有这两个字段时回退为「空间 + ID 前 8 位」。
## 14. 文件管理器交互
文件区支持单击选中、Ctrl/Command 多选、Shift 连选、双击预览、右键菜单和键盘 Enter/Delete/Ctrl+A/Escape。文件夹单击选中、双击或菜单“打开文件夹”进入;面包屑用于快速返回。文件行提供更多菜单按钮,触屏也能访问操作。
@@ -349,9 +357,13 @@ Share 是独立的只读发布资源,不创建频道成员、threads 或席位
## 15. 频道中的文件消息与发送者
聊天时间线同时显示文本和已发布的文件,文件消息使用头像、发送者名称、时间和文件卡片,卡片提供预览和下载。当前账号发出的消息靠右,其他人的消息靠左;没有头像时显示名称首字。上传中显示进度和状态,受限文件不自动加载内容或媒体缩略图。
聊天时间线采用 Discord 式布局:所有消息靠左,同一条左边线,左侧头像、右侧「名称 + 时间」一行与占满宽度的正文,不再区分自己和他人。同一发送者 5 分钟内连发的消息合并,只缩进对齐,悬停时在头像位置显示时间;悬停整行有淡底,右上角出现删除。文件消息是 360px 以内的文件卡片(图标、名称、大小与类型,右侧预览与下载);没有头像时显示名称首字。受限文件不自动加载内容或媒体缩略图。
发送者名称与头像从后端账号资料读取,回退固定用户名;设备名保留在提示中。资源作者由后端记录,昵称修改后其他设备同步更新展示。没有账号归属时回退设备来源。
输入区是整体圆角框(左 + 附件、中间自增高输入、右发送),占位文案「给 #频道名 发消息」,Enter 发送、Shift+Enter 换行(输入法组合中不触发)。
**乐观发送**:点发送的同一帧就清空输入框并在时间线底部画出消息(半透明,aria-busy),后台再加密上传,不等后端;可以连续快速发送。worker 发布真实条目时带回 localId,在同一次状态更新里替换占位项,不闪烁、不重复。失败时占位项保留原文本并显示「发送失败 · 重试」,可删除;离线沿用本机发送队列。文件同理:确认上传选项后卡片立刻出现并显示进度。
发送者名称与头像从后端账号资料读取,回退固定用户名。资源作者由后端记录,昵称修改后其他设备同步更新展示。没有账号归属(未登录访客)时统一显示「访客」;界面里不再展示设备名,设备只在账号设置的「我的设备」里管理。
头像缩略图在 Worker 内生成,最大 64×64、编码后不超过 8 KB,仅使用 PNG/JPEG/WebP data URL;不使用第三方头像链接。圆形头像使用专门的 --radius-avatar token,消息气泡仍遵守现有圆角规范。