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>
- FileAttachment never surfaced a failed authenticated download -- the
card looked identically clickable whether it worked or not, and
clicking while broken silently no-op'd. Added an error state/retry
(useAuthedBlobUrl gained a retry() escape hatch for this).
ImageAttachment/AudioAttachment already handled this; only the file
card didn't.
- --shadow-btn was one token serving two different specs: round 2's fix
(0 6px 16px, from the 50px auth CTAs) actually regressed the 36px
messaging send button (ConversationView/VoiceRecorder), which needs
0 4px 12px per 02-messages.dc.html. Split into --shadow-btn /
--shadow-btn-sm.
- loadOlder's scroll-restore guard compared topicId by value, which
false-positives on an A->B->A round trip within one request's
lifetime (same string, but the DOM was torn down and rebuilt by the
intervening open/close). Switched to a generation counter, same
pattern as topics.ts's leave-race fix.
- errorToast wasn't included in ConversationView/DetailColumn's
topicId-reset effects -- a failed action's toast could keep showing
over a conversation/contact switched to afterward. Also switched
those effects (and MessagesLayout's) from useEffect to
useLayoutEffect -- since none of these components remount on
topicId change, a post-paint effect could let one frame render with
the previous topicId's stale UI still showing.
- VoiceRecorder never listened for MediaRecorder's own onerror -- a
mid-recording failure left the "录音中" timer ticking forever with no
indication the recording had already died.
- VerificationCodeButton's phone-change countdown reset (round 2) only
cleared the button's own state; RegisterPage/ResetPasswordPage's
challengeId was still tied to whatever phone the code was actually
issued for, with nothing cross-checking it against a since-edited
phone number.
- Agreement's checkbox used bare accent-color on a native input, which
only tints the checked fill and leaves size/radius/border to the
browser/OS; replaced with the spec's exact 16x16 border-radius:4px
box + check glyph.
- Added a catch-all route (App.tsx), TenantFeedbackPage image-picker
validation feedback + corrected remove-button style, and a handful of
smaller token/spacing corrections against screens/*.dc.html (NavRail
font-weight, LoginPage/TenantEntryPage logo size+shadow+color,
ChatListColumn row padding/icon size, --shadow-modal alpha).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- DetailColumn: remark-edit draft and the history-search modal's open
state weren't reset on contact switch (no key={topicId}, same long-
lived instance) -- could silently save a typed remark against the
WRONG contact. History-search state lifted to MessagesLayout so
ConversationView's header search icon (previously wired to nothing)
can open the same modal
- ConversationView: loadOlder's in-flight "restore scroll position"
callback and its loading flag weren't guarded against a conversation
switch happening mid-request, and the scroll-to-bottom effect keyed
only on messages.length so two conversations that happened to load
the same page size never got scrolled down on open
- ChatListColumn: pinned 系统消息/文件助手 rows used solid-color-square
avatars like regular contacts; the actual design-code source
(ChatList.dc.html) and reference screenshots use a light tinted
background with a colored icon instead
- MessageBubble: own ("我") messages hash-colored their avatar like any
contact instead of the design's fixed accent blue
- Every send/recall/favorite/forward action swallowed its own failure
silently (empty .catch or, for forward, no catch at all) -- moved
Toast from features/auth into app/ (shared) and wired real error
feedback through it
- MessageContextMenu: added the recall countdown label, Escape-to-close,
and viewport-edge clamping (a right-click near the window edge could
render partially off-screen)
- EmojiPanel now defaults to 最近 per the reference (was defaulting to
表情)
- HistorySearchModal: pagination (searchTopicMessages/getTopicMedia both
supported cursor/next_cursor already, nothing used it -- silently
truncated at one page), Escape-to-close, and a brief "没有找到" flash
on the first keystroke before the debounce caught up
- A handful of design-token corrections (--shadow-btn, NavRail's border/
spacing, LoginPage's logo radius/brand-panel padding, a placeholder
icon color) verified against screens/*.dc.html's exact pixel values
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>