diff --git a/docs/superpowers/plans/2026-09-30-web-brand-and-message-sound.md b/docs/superpowers/plans/2026-09-30-web-brand-and-message-sound.md new file mode 100644 index 00000000..d28be5ee --- /dev/null +++ b/docs/superpowers/plans/2026-09-30-web-brand-and-message-sound.md @@ -0,0 +1,151 @@ +# Web Brand And Message Sound Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将生产 Web 品牌统一为“迅工捷使”,精简登录页,并为符合提醒条件的新消息播放用户提供的提示音。 + +**Architecture:** 继续以 `web/src/lib/config.ts` 作为显示品牌名来源,认证页和系统消息只消费该常量;品牌图标和 MP3 作为 Vite `public/` 静态资源。消息声音接入现有 `push.ts` 新消息扫描链路,在浏览器通知权限判断之前复用“本人消息、当前可见会话、会话免打扰”的过滤结果。 + +**Tech Stack:** React 19、TypeScript、Vite、Node test runner、Jiti。 + +--- + +### Task 1: 写品牌和提示音红测 + +**Files:** +- Create: `web/tests/brand-and-notification-sound.test.mjs` + +- [ ] **Step 1: 写失败测试** + +创建 Node 测试,核心断言如下;可按实际导出补齐 fake `Audio` 的类型字段,但不得降低断言: + +```js +const webRoot = new URL('../', import.meta.url); +assert.match(await readFile(new URL('index.html', webRoot), 'utf8'), /迅工捷使<\/title>/); +assert.doesNotMatch(await readFile(new URL('src/features/auth/LoginPage.tsx', webRoot), 'utf8'), /桌面端|下载桌面客户端|iOS \/ Android|帮助中心/); +assert.doesNotMatch(await readFile(new URL('src/features/auth/TenantEntryPage.tsx', webRoot), 'utf8'), /MeetingIM/); +assert.doesNotMatch(await readFile(new URL('src/features/system/SystemMessagesView.tsx', webRoot), 'utf8'), /MeetingIM 安全团队/); +assert.equal(audioSource, '/message-notification.mp3'); +assert.equal(playCount, 1); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `node --experimental-strip-types --test tests/brand-and-notification-sound.test.mjs` + +Expected: FAIL,因为品牌仍是 `MeetingIM`,提示音模块和静态资源尚不存在。 + +### Task 2: 更新品牌和登录页 + +**Files:** +- Modify: `web/index.html` +- Modify: `web/src/lib/config.ts` +- Modify: `web/src/features/auth/LoginPage.tsx` +- Modify: `web/src/features/auth/TenantEntryPage.tsx` +- Modify: `web/src/features/system/SystemMessagesView.tsx` +- Create: `web/public/app-icon.png` +- Delete: `web/public/favicon.svg` +- Modify: `web/src/lib/notifications/tabIndicator.ts` +- Modify: `web/src/lib/notifications/push.ts` + +- [ ] **Step 1: 复制官方品牌图标** + +将 `../im-ios-website/dist/assets/app-icon.png` 原样复制为 `web/public/app-icon.png`,所有 Web 图标引用统一指向它。 + +- [ ] **Step 2: 最小化更新可见品牌** + +把浏览器标题、认证页和系统消息署名改为“迅工捷使”;`APP_NAME` 保留 `MeetingIM/<version>` 作为现有 Tinode 客户端技术标识,避免把显示品牌替换扩大成协议改名。 + +```ts +export const BASE_APP_NAME = '迅工捷使'; +export const APP_NAME = `MeetingIM/${__APP_VERSION__}`; +``` + +认证页统一使用: + +```tsx +<img src="/app-icon.png" alt="" className="h-full w-full rounded-[inherit]" /> +<strong>{BASE_APP_NAME}</strong> +``` + +- [ ] **Step 3: 精简登录页** + +说明文案改为“单聊、群聊、通讯录、文件与收藏、音视频通话,消息与回执在手机端保持一致。”,删除底部三个入口。 + +```tsx +<p className="mt-4 max-w-md text-base text-white/80"> + 单聊、群聊、通讯录、文件与收藏、音视频通话,消息与回执在手机端保持一致。 +</p> +``` + +### Task 3: 接入消息提示音 + +**Files:** +- Create: `web/src/lib/notifications/sound.ts` +- Modify: `web/src/lib/notifications/push.ts` +- Create: `web/public/message-notification.mp3` + +- [ ] **Step 1: 复制用户提供的提示音** + +将 `/Users/aa/Downloads/universfield-new-notification-012-363675.mp3` 原样复制为 `web/public/message-notification.mp3`。 + +- [ ] **Step 2: 实现安全播放器** + +首次需要时创建 `Audio('/message-notification.mp3')`,每次播放前设置 `currentTime = 0`;对同步异常和 `play()` Promise 拒绝均静默处理。 + +```ts +let messageAudio: HTMLAudioElement | null = null; + +export function playMessageSound(): void { + try { + messageAudio ||= new Audio('/message-notification.mp3'); + messageAudio.currentTime = 0; + void messageAudio.play().catch(() => {}); + } catch { + // Optional audio must never interrupt message delivery. + } +} +``` + +- [ ] **Step 3: 接入新消息过滤链路** + +把“本人消息、当前可见会话、会话免打扰”抽成通知与声音共享的判断。通过判断后先播放声音;仅当 Notification API 可用且权限为 `granted` 时再显示系统通知。 + +```ts +if (!shouldAlert(topicName, latest)) return; +playMessageSound(); +if (typeof Notification !== 'undefined' && Notification.permission === 'granted') { + fireNotification(topicName, latest); +} +``` + +- [ ] **Step 4: 跑绿测** + +Run: `node --experimental-strip-types --test tests/*.test.ts tests/*.test.mjs` + +Expected: 现有 6 项和新增测试全部 PASS。 + +### Task 4: 验证、提交与部署 + +**Files:** +- Verify only + +- [ ] **Step 1: 静态验证** + +Run: `npm run build && npm run lint` + +Expected: 构建退出码 0;lint 无 error,允许记录既有 warning。 + +- [ ] **Step 2: 完整性验证** + +Run: `git diff --check` + +Expected: 无输出,退出码 0;MP3 和应用图标的 SHA-256 与来源文件一致。 + +- [ ] **Step 3: 提交并推送** + +提交精确修改文件到 `dev`,推送前先 fetch 并确认无远端分叉。 + +- [ ] **Step 4: 部署和公网回归** + +触发 Gitea `deploy.yml`,等待成功,确认 K3s Deployment 2/2;访问 `https://web.118im.com` 验证标题、品牌、登录页删减及双账号新消息提醒路径。 diff --git a/docs/superpowers/specs/2026-09-30-web-brand-and-message-sound-design.md b/docs/superpowers/specs/2026-09-30-web-brand-and-message-sound-design.md new file mode 100644 index 00000000..b5397a47 --- /dev/null +++ b/docs/superpowers/specs/2026-09-30-web-brand-and-message-sound-design.md @@ -0,0 +1,34 @@ +# Web 品牌与消息提示音设计 + +## 目标 + +将当前生产 Web 的用户可见品牌统一为“迅工捷使”,精简登录页指定文案,并使用用户提供的 MP3 为新收到的消息播放提示音。 + +## 范围 + +- 当前 Dockerfile 实际构建的 `web/` 目录。 +- 浏览器标题、企业码页、登录页、系统消息署名和应用图标。 +- 登录页说明改为“消息与回执在手机端保持一致”,删除底部三个入口。 +- 复用 Android/iOS 已有蓝底扳手图标。 +- 新消息提示音使用用户提供的 `universfield-new-notification-012-363675.mp3`。 + +## 行为 + +- 只为他人发来的新消息播放一次提示音。 +- 正在可见地查看对应会话时不播放。 +- 会话设置为消息免打扰时不播放。 +- 声音播放失败不得影响消息接收、列表更新或浏览器通知。 +- 浏览器系统通知权限不控制声音;声音与现有通知共用同一条新消息判定链路。 + +## 明确不改 + +- 后端代码和后端默认推送、短信名称。 +- `X-MeetingIM-APIKey`、`X-MeetingIM-Auth` 等协议头。 +- `meetingim-session` 等已有存储键。 +- 仓库名、镜像名、域名和旧版未部署 Web 入口。 + +## 验证 + +- 自动测试覆盖品牌文字、登录页删减、品牌图片引用和提示音播放器。 +- Web 全量测试、构建、lint 和 `git diff --check`。 +- 部署后实测企业码页、登录页、浏览器标题、图标,以及双账号新消息提示音路径。