Commit Graph
6 Commits
Author SHA1 Message Date
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 93d4077d1e fix(web): code review round 1 remaining UI fixes
- EmojiPanel: add missing 手势/物品 categories, 12-column grid per README §3.3
- DetailColumn: add 投诉举报 row, real ID/region/签名 via new getUserProfile
  API, chevron_right icons now use --color-icon-arrow token (were tertiary
  text color), regroup rows so dividers only sit between sections not every
  row (matches design-code reference exactly)
- MessageBubble/ConversationView: right-clicked message bubble now gets the
  spec's outline while its context menu is open
- ChatListColumn/AppShell: pinned 系统消息/文件助手 rows using confirmed
  bootstrap entries/entry_badges topic names ("sys"/"slf"); route to the
  existing SectionPlaceholder pattern since their own dedicated layouts are
  Phase 8, not built yet

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 03:58:06 +08:00
Your NameandClaude Sonnet 5 a7da4dc079 fix(web): code review round 1 -- messaging UI critical/high findings
- CRITICAL: draft text and voice-recording state were never isolated per
  conversation. ConversationView is one long-lived instance reused across
  every topic (`:topicId` route param change doesn't remount it), so a
  single `draft` string leaked one conversation's unsent text into whatever
  conversation was opened next -- and, far more seriously, starting a
  voice recording in conversation A and then switching to conversation B
  before hitting send would publish that recording into B: VoiceRecorder
  keeps its own MediaRecorder running across the switch, but the onSend
  callback passed to it is a fresh closure over whatever topic is CURRENT
  when it finally fires. Fixed by keying drafts per topicId and force-
  resetting `recording` (unmounting VoiceRecorder, which stops its stream)
  whenever topicId changes.
- CRITICAL: the detail/settings panel was completely unreachable below
  960px -- it only ever rendered inside `lg:block` (>=1280px, docked) or
  `md:block lg:hidden` (960-1279px, drawer) wrappers, so tapping the header
  "i" button below 960px flipped internal state and fired its data queries
  with zero visible effect. Added the README §1.4-mandated <960px full-page
  overlay tier, and a small new useMediaQuery hook so the header's "i"
  button highlight reflects whether the panel is ACTUALLY visible right
  now instead of being hardcoded to a breakpoint (it was previously always
  blue at 960-1279px regardless of the drawer's real open state, and always
  grey at >=1280px despite the panel being permanently docked open there).
- CRITICAL (visually confirmed against 1a-desktop-chat.png): messages you
  send never showed an avatar at all (just an empty spacer) -- every
  reference screenshot shows your own "我" avatar beside your bubbles the
  same as the other party's. MessageRow now takes myUserId and renders it.
  While in there, also moved the failed-send error icon beside the bubble
  (README: "气泡左侧") instead of below it with the retry text.
- CRITICAL: ChatListColumn's search box had no value/onChange at all --
  typing into it did nothing. Now filters by contact name; added the
  search_off/"未找到相关会话" and forum/"暂无会话" empty states README §2.1
  calls for, neither of which existed before.
- MEDIUM (real Tailwind cascade bug): the selected chat-list row's
  highlight was being overridden by the unconditional `hover:bg-[...]`
  class on the same element -- Tailwind emits hover-variant rules after
  base rules regardless of source order, so hovering an already-selected
  row visually un-selected it until the mouse moved away. Scoped the hover
  class to the non-selected branch only.
- NavRail: inactive icons used the wrong color token (icon-default #555c65
  instead of the spec's #656b74, this app's text-secondary); the unread
  badge prop was never actually supplied by AppShell, so the badge's
  otherwise-pixel-correct rendering never appeared in the running app.
  Fixed both, and gave BottomTabBar's 消息 tab the same real badge (computed
  once in AppShell from useChatList(), passed to both).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 03:23:54 +08:00
Your NameandClaude Sonnet 5 59d1033a92 feat(web): image and file message attachments (send + render)
- lib/api/upload.ts: POST /v0/file/u/ (docs/IM-前后端对接文档.md:460-461) --
  a separate surface from /v1/im/*, at the tenant's file base URL, auth'd
  with X-MeetingIM-Auth: token <token> rather than Authorization: Bearer
  (matches upstream Tinode's own LargeFileHelper convention).
- lib/tinode/attachments.ts: sendImage/sendFile. Files <= the inband limit
  go in directly as base64 (Drafty.insertImage's `bits` field / attachFile's
  `data` field) with no upload round trip; larger ones upload first and
  reference the returned URL. Simplified from the old app's urlPromise
  dance (optimistic local preview while the upload runs in the background)
  by just awaiting the upload before creating the message -- less
  optimistic, meaningfully simpler.
- AttachmentPreview.tsx: image/file bubble rendering. Externally-referenced
  (non-inline) attachments need an authenticated fetch into a blob URL,
  not a plain <img src>/<a href> -- GET /v0/file/s/{file_id} requires the
  same custom auth header uploads do, which a bare element can't attach.
- drafty.tsx: wired the 'IM'/'EX' Drafty style-tag cases to these.

Verified end to end: sent a real inline image (confirmed the exact IM
entity shape in the outgoing WS packet, and that the <img> decoded and
loaded from its own base64 data) and a real inline file (rendered with the
correct icon/size/download affordance).

Known gap: voice messages (MediaRecorder + AU entities) not built yet --
meaningfully more complex (hold-to-record gesture, audio encoding,
duration/waveform) and deliberately left for a following pass rather than
rushed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 02:44:12 +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
Your NameandClaude Sonnet 5 8bcd9f3a5b 新分支:Vite+React19+TS+Tailwind v4 重新搭建,先跑通四级响应式布局壳
不再沿用旧 im_webapp 的技术栈(webpack + class组件 + 15000行全局CSS + 手搓History路由),
按业务重新选型:Vite/React Router/Zustand预留/TanStack Query/保留tinode-sdk真实依赖
(协议客户端本身没有重写的业务理由)。新代码放在 web/ 子目录,旧代码原样保留在仓库根目录,
两者共存到重写完成。

这一步先验证布局壳本身:NavRail(消息/通讯录/收藏/文件助手/通话记录/设置)+ 会话列表 +
主区域 + 详情栏,按设计稿的四档断点(<640/640-959/960-1279/≥1280)用真实浏览器宽度逐档验证
过——两列及以下"列表/主区域二选一+返回键"、三列"详情栏抽屉化"、四列"详情栏常驻"均已用
真实点击/返回验证正确,不是只看了截图。会话列表和聊天内容目前是占位数据,业务数据接入和
Tinode连接是下一步。

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