Commit Graph
10 Commits
Author SHA1 Message Date
Your Name 7fff1a7b3a fix: call media-connect state, blocklist banner staleness, group rename, avatar URL
- stores/calls.ts, CallRoot.tsx: distinguish signaling-connected from
  actual media-joined (mediaJoined flag), so a call whose mic never
  attaches no longer shows a live timer/"网络良好"; propagate a
  disconnected reason on post-accept media failure so both sides agree
  on the outcome instead of one side seeing a normal "已结束".
- DetailColumn.tsx: invalidate conversation-context alongside blocks on
  block/unblock, so the composer's blocked-notice banner updates
  immediately instead of only after a window-focus refetch.
- GroupDetailColumn.tsx: add an owner-only group rename control
  (updateGroup was already implemented but never wired to any UI).
- Avatar.tsx: stop re-wrapping an already-absolute avatar_file_id (seen
  on default-avatar placeholders) into a malformed
  /v0/file/s/https://... request.
2026-09-18 15:01:08 +08:00
Your Name 0c5b329179 修复Zentao批量报告的6个前端bug:来电UI不弹出/黑名单发送无提示/群名校验/群人数与通话记录显示uid
用户提供了一份Zentao导出的bug清单(#21/23/24/25/26/27/28共7条),逐条实机复现
排查,修复了其中6个真正的前端bug,1个是纯后端问题(禁言不生效)已记录到
docs/IM-后端问题清单.md,1个(好友申请404/500)未能独立复现只记录了测试团队
的原始报告,另1个(离线状态推送)实测后确认并非报告描述的"永久不更新",只是
有正常的心跳超时延迟。

- **#23 来电UI完全不弹出**(根因最深的一个):initCallSignaling用的是
  `me.getMeTopic().contacts()`枚举联系人再查最新消息——但这个方法只反映`me`
  话题自己的订阅同步进度,一个P2P话题可能早就真正订阅上、能收到消息,`me`的
  同步却还没跟上,导致这个联系人在枚举里直接消失。用两个真实账号实机验证:
  被叫方`me.contacts()`只枚举出1个群,主叫方完全没出现,来电消息静默丢失。
  topics.ts里`getChatListSnapshot()`自己的注释早就写明了这个坑、并且已经改
  用`mapTopics()`规避,只是`stores/calls.ts`这个监听器当时没有跟着改。现在
  统一复用`getChatListSnapshot()`,实机验证来电卡片正确弹出。

- **#28 拉黑后发送消息无原因提示**:读tinode-sdk源码确认发送失败的具体原因
  从架构上就拿不到——`Topic#publishMessage`自己的`.catch()`把服务端拒绝原因
  吞掉、只留一个`_failed`标记,不会重新抛出。改成事前判断:新增
  `getConversationContext()`(P2P专用,`relationship`字段区分
  `blocked_by_me`/`blocked_by_peer`),发送区直接换成对应文案的横幅,两个
  方向都拿真实账号互相拉黑验证过。

- **#26 群名超长/留空自动名两个校验问题**:`MAX_TITLE_LENGTH`一直是60,但
  后端`im_http.go`真实校验的是32字(读源码确认),导致33-59字之间的群名能
  输入、能提交,然后被后端拒绝只显示通用错误。改成32,从输入框这一层就不
  再放行超长名。另外群名留空时自动拼接的成员昵称组合,理论上可能被内容审核
  拒绝——这种情况下用户根本不知道"名字"是问题所在,补了针对性提示。

- **#27 通话记录/群列表显示原始uid、群人数显示0人**(2处独立bug):
  1. `CallHistoryView.tsx`按`peer_user_id`(真实usr id)去匹配聊天列表的
     `contact.topic`(建立过真实会话后已经是服务端重命名过的p2p哈希),两个
     不是同一个id空间,永远匹配不上,一直显示原始id。改成用`item.topic`
     (已确认和chatlist的topic是同一个值)去匹配。
  2. 实测发现`GET /groups`列表接口的`member_count`会返回0,但同一个群
     `GET /groups/{topic}`单独查询正确返回4——两个接口的计数器不一致(后端
     已知问题,见问题8),前端这次统一改成"读到0就不显示人数"而不是显示一
     个已确认错误的数字。

以上改动均跑过`tsc -b --noEmit`/`oxlint`/`vite build`,除#26外全部用两个真实
账号(systest02/systest04)互相操作实机验证。
2026-09-17 21:51:00 +08:00
Your NameandClaude Sonnet 5 cd6b7675d4 新消息提示:标签页标题滚动+未读数、favicon角标、桌面通知(含何时不弹的判断)
三件套,参照Slack/Telegram Web/WhatsApp Web等主流IM网页客户端的标准做法:

- lib/notifications/tabIndicator.ts:标签页被切到后台且有未读时,标题在
  "📨 (N条新消息) <原标题>"上做左向滚动,用户切回来的瞬间(visibilitychange)
  立即恢复原标题,不等下一次未读数变化才发现。favicon右上角画一个红色圆点+
  白色数字角标(canvas绘制成PNG data URL,写回现有<link rel="icon">,不是
  另加一个可能被浏览器忽略的竞争链接)。两者都由AppShell.tsx里已经算好的
  同一个unreadMessages号驱动,不另起一套可能对不上的计数。

- lib/notifications/push.ts:真正的浏览器桌面通知(Notification API)。只
  在标签页还开着(哪怕最小化/切到后台)时生效——做不到浏览器整个关掉也能收到,
  那需要Service Worker+推送订阅+后端配合,是完全另一个量级的工程。

  抑制规则("什么时候不弹"):
  · 消息来自自己(比如另一个标签页/设备同步过来的回显)
  · 正在看的就是这个会话本身(会话已打开且标签页确实可见——两者都要满足,
    一个被切到后台的标签页哪怕挂载着这个会话也不算"正在看")
  · 该会话设置了"消息免打扰"(contact.private.mute_notifications)
  · 浏览器未授予通知权限
  同一会话的连续通知用tag合并显示为一条,不会叠加刷屏;点击通知会聚焦标签页
  并跳转到对应会话(通过stores/notificationNav.ts中转,push.ts在React之外,
  没有useNavigate可用,做法与stores/calls.ts、messageJump.ts是同一个模式)。

  判断"是否是真的新消息"用的是防抖后的seq对比(防止刚打开一个从未见过的会话时,
  历史消息分批到达被逐条误判成"新消息")——第一次见到某个会话时记录基准而不是
  照单全收。**这里踩到一个真实bug并当场修复**:最初版本把"第一次见到这个会话"
  一律当成"历史消息,不通知",结果全新好友的第一条消息(对这个会话来说seq=1,
  同时也是"第一次见到")被误判成历史消息静默吞掉——用两个全新互加好友的测试
  账号实机复现后,改成额外看消息时间戳:一分钟以内的算真正新消息,无论是不是
  第一次见到这个会话,否则才当历史记录处理。

实机验证(systest01/systest02两个全新互加好友的测试账号,全部通过mock
Notification类捕获+SDK本地状态直接读取的方式验证,因为这个自动化浏览器环境
本身把Notification.permission锁死成denied,没法测试真正弹出的系统通知):
- 真实新消息:通知的title/body/tag均正确(对方昵称/消息预览文本/会话id)
- 免打扰会话:确认不弹
- 已打开且标签页可见:确认不弹
- 标题栏滚动+favicon角标:实机确认能正确开始/停止滚动、favicon生成的是
  真实可解码的64×64 PNG

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 20:14:12 +08:00
Your Name 8013c1675f 修复禅道Bug清单中的前端问题:好友申请source校验、群解散二次验证缺失、
群解散事件未处理、错误码翻译缺失、恢复登录页误导性提示

- 好友申请首次必失败30000(P0):AddFriendModal把手机号搜索单独标记为
  source:"phone"上报,但后端serveIMFriendRequestCreate的source校验只认
  search/qr/group,命中default分支返回"invalid friend request source"。
  改为手机号搜索也统一上报source:"search"(后端本就不区分具体搜索渠道),
  用两个全新、此前无任何关系的测试账号验证首次申请200成功
- 解散群聊需要短信二次验证但完整流程从未实现(P0):新增
  SecurityChallengeModal通用二次验证弹层(POST /security/challenges→
  验证码→verify→action_token)+ requestActionToken()(stores/
  securityChallenge.ts,与requestConfirm同一模式),接入解散群聊和转让
  群主(后者此前也因缺X-Action-Token必然404/400)。同时发现并修复
  imDelete对无body请求不显式传空对象体,导致携带Content-Type:
  application/json但请求体真正为空时后端JSON解码失败返回400——实测确认
  两个问题都解决后DELETE返回200
- 群聊被解散但当前正查看该群的用户界面不会跳转(P1):新增
  topics.ts的subscribeTopicGone()监听SDK自身的onDeleteTopic回调,
  ConversationView据此在当前会话被解散时弹出提示并跳转回消息列表
- P2P会话发起方标题栏显示原始topic id、对方明明在线却显示离线:确认
  为上一轮已修复的isCommType()根因问题的同一症状,本轮实测(新建从未
  聊过天的账号对)已不再复现;群内消息发送者显示原始uid的关联描述同样
  实测未复现
- 好友关系冲突(已是好友/申请正在处理中)报错英文原文透出:翻译表补充
  FRIEND_REQUEST_CONFLICT
- 深链/刷新页面时的"改为重新登录"链接此前在任何恢复登录过程中都会
  立即显示(不管是否超时),造成短暂白屏时的误导性提示:改为仅在真正
  超时后才显示该链接,与其上方的提示文案保持一致的显示条件
2026-09-16 15:42:41 +08:00
Your Name 29a1f1c2fd 修复收藏/系统消息/通话记录批次的代码审查问题
- 通话结果"已拒绝"文案按视角区分(拒绝方看到"已拒绝"而非"对方已拒绝")
- 通话记录页补充移动端返回头部与加载失败重试态
- 挂断时按是否已接通传递正确的挂断原因(cancelled)
- 系统消息"全部标为已读"翻页加循环护栏,防止游标卡死;按钮可见性改用真实未读总数
- 收藏批量删除/转发在部分失败时保留失败项以便重试,而非无条件退出多选模式
- 收藏卡片边框/分类文字/侧栏图标颜色token修正
2026-09-16 12:54:40 +08:00
Your NameandClaude Sonnet 5 37d08e8989 feat(web): 用符合系统风格的组件替换全部 window.alert/confirm
全项目搜出15处 window.confirm、9处 window.alert(其中3处是各文件
各自复制的 notImplemented() 辅助函数,文案还都一样)用在退出登录、
删除消息/收藏/好友、清空聊天记录、解散群聊、转让群主、危险链接
二次确认等场景——全是浏览器原生弹窗,样式和整个应用的设计语言完全
脱节。

新增两个全局UI基础设施(Zustand store + 挂载在App.tsx根节点的
渲染组件,参照stores/calls.ts/messageJump.ts已有的"任何组件都能调用"
惯例,不是每个弹窗各自管理一份本地状态):

- stores/confirmDialog.ts + app/ConfirmDialogRoot.tsx:替换
  window.confirm。requestConfirm()返回Promise<boolean>,和
  window.confirm的返回值语义一致,所以原来"if (!window.confirm(...))
  return"这一行改成"if (!(await requestConfirm({message:...})))
  return",外层函数标成async,不用大改周围逻辑。危险/不可恢复操作
  (删除、解散群聊、注销账号等)传danger:true,确认按钮变红色,
  弹窗视觉沿用转发弹窗已有的"描边取消+实心确认"按钮搭配。
- stores/globalToast.ts + app/GlobalToastRoot.tsx:替换window.alert,
  复用应用里本来就有、每个页面各自本地状态管理的Toast组件。顺手把
  3个文件里各自重复定义、文案完全一样的notImplemented()合并成
  一个共享导出。

实机验证了非危险确认(清空缓存,蓝色确认按钮)、危险确认(删除
消息,红色确认按钮)、取消按钮、Escape键取消、全局提示(点"投诉
举报")——均正常工作,且原有页面各自的Toast错误提示不受影响。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 00:08:15 +08:00
Your NameandClaude Sonnet 5 46b3288880 feat(web): 下线自主注册与协议勾选,修复转发消息报错,转发选择器分组+全选
按产品决定移除面向国内客户不适用的自主注册入口:删除 /register 路由
及 RegisterPage、登录页"马上注册"链接、登录/注册共用的协议勾选组件
Agreement,以及仅注册流程使用的 registerAccount/queueAccExtra 等
死代码;登录不再依赖勾选协议才能提交。

修复转发消息报错(真实高频bug,不限于个别场景):`forwardMessage`/
`sendText` 发送前只是 `tinode.getTopic()` 取一个本地话题对象,从未
真正订阅——SDK 要求话题处于 attached 状态才允许发布,而只有当前正
打开的会话才满足这一点,一旦切换过/离开过某个会话(哪怕之前订阅过
预览),它就会变回未订阅状态。也就是说转发给"当前会话之外"几乎任何
一个联系人都会失败。新增 `ensureTopicAttached()`,在转发前按需临时
订阅目标话题,单条转发和多选批量转发两处都已接入,实机验证对未打开
过的好友/群转发均成功。

转发选择器(ForwardPickerModal)按好友/群聊分组显示(原来混在一个
列表里),并加了"全选/取消全选",避免多个接收方时逐条点选。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 19:04:59 +08:00
Your NameandClaude Sonnet 5 5c3f3b7653 feat(web): complete groups/contacts/favorites/settings/calls/system domains + extensive review-round fixes
Rounds out the full rewrite with the remaining feature domains: group
management (admin panel, mute, files, announcements, QR-based invite/join),
contacts (friend profile, add-friend, blocklist, new-friends), favorites
(real media rendering across text/image/video/voice/file/link), settings
(profile/security/privacy/general/about), calls (Agora-backed audio/video
signaling + call history), and system messages/file assistant.

Also folds in a large batch of live-verified fixes surfaced by repeated
fresh code-review passes and hands-on E2E testing against the real
backend, including: WS reconnect not resubscribing any topic after a
disconnect, missing x-im-client-mid on every send path (protocol
compliance for retry dedup), group mute/member-settings/self-profile
endpoint corrections, relationship-state type fixes across search/QR/
friend-profile, tenant-switch logout gap, password-policy now read from
the real per-tenant config instead of guessed, NavRail highlight/
aria-current desync, several call-state-machine races, and responsive
layout fixes across all four breakpoints (mobile/tablet/laptop/desktop)
including a full mobile rebuild of favorites, call UI, and the settings
home/sub-list split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:16:33 +08:00
Your NameandClaude Sonnet 5 d3a0eb0dbc fix(web): code review round 2 -- critical session/connection races
- RequireAuth: a page reload lost the session even with a valid token
  sitting in sessionStorage -- myUserId is never persisted (by design)
  but nothing ever re-derived it from the reusable login token. Added
  connection.ts's restoreSession(), which also fixes the race the first
  attempt at this ran into live: React 19 StrictMode double-invokes the
  restore effect in dev, and Tinode.loginToken() isn't idempotent (second
  call on an already-authenticated connection gets 409'd), so both the
  connect() and the login itself needed module-level in-flight dedup,
  not just a per-effect cancelled flag
- topics.ts: same dedup pattern for openConversation/closeConversation --
  StrictMode's mount->cleanup->remount always overlapped an open with
  itself, and a leave() landing before its own subscribe() resolved was
  silently no-op'ing instead of detaching
- LOGGING_ENABLED was unconditionally on, including prod builds --
  Tinode's verbose logging prints the raw login secret and full message
  content to the console
- imPost/imPatch/imPut/imDelete could silently drop the Idempotency-Key
  header for a caller that passes its own headers (object-spread order
  bug, dormant today since no call site does this yet)
- DetailColumn's getFriends()/getBlocks() only ever read page 1 despite
  fetchAllPages existing specifically to prevent this bug class
- availableActions() now defers to the server's message_policy.
  supported_actions when present instead of only a client-side time
  window, and exposes recallRemainingSeconds for the context menu's
  countdown label

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 04:48:28 +08:00
Your NameandClaude Sonnet 5 8cdd84152d 企业码解析+登录跑通真实后端:Tinode连接适配层、类型化API客户端、会话store
lib/tinode/connection.ts:单例Tinode连接+自己的发布订阅层——SDK的onConnect/onDisconnect/onLogin
是单槽位属性(赋值会互相覆盖),不是真正的多监听器事件源,所以这里统一接管这几个槽位,自己
扇出给任意数量的React订阅方(lib/tinode/hooks.ts的useSyncExternalStore)。installTenantHiPatch
照搬旧应用tinode-web.jsx的实现搬过来——服务端{hi}包的tenant字段SDK本身没有API,只能在原始
sendText层面注入,这是后端协议的真实约束,不是旧代码库特有的写法。

lib/api/client.ts:REST客户端按旧应用src/lib/im-api.js的request函数+完整REASON_MESSAGES错误码
翻译表搬过来(含请求头X-MeetingIM-APIKey等几个月联调换来的正确写法),这次是重新用TypeScript
写、不是复制文件,但端点行为对齐。

实机联调过程中发现并修正了一处凭记忆猜错的地方:tenants/resolve真实响应结构是
{endpoints:{api,file,websocket}, tenant:{code,name}},不是我最初假设的顶层{code,name}——
已用真实请求验证修正。用企业码default解析成功(显示"Default Tenant")+ 一次真实错误密码
登录尝试(服务端真实401,错误提示正确显示"账号或密码错误")两步都对着真实后端验证通过,
不是只测了正确路径。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 00:38:50 +08:00