From 9271a0a9c120a6efa896612d55c04dd2ca389e6c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 9 Sep 2026 23:35:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E9=9A=90=E8=97=8F=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=8A=A9=E6=89=8B=E5=85=A5=E5=8F=A3=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=87=AA=E8=81=8A=E5=A4=A9=E8=AF=A6=E6=83=85=E6=A0=8F?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端目前没有支持文件助手所需的配额/分类等能力,只是把它当作 普通话题处理;与其做一个功能不全的界面,先从导航栏、会话列表 置顶区、通讯录固定入口、设置页移动端快捷入口中移除该入口。slf 话题现在按普通会话行为处理(排序、未读计数均不再特殊处理)。 同时修复 DetailColumn 对自聊天(slf)显示语音/视频通话、备注、 拉黑、举报、删除好友等对自己无意义操作的问题,改为仅显示适用 的发消息/查找聊天记录/置顶/免打扰/清空聊天记录等项。 Co-Authored-By: Claude Sonnet 5 --- web/src/app/AppShell.tsx | 14 +-- web/src/app/NavRail.tsx | 18 +--- .../features/contacts/ContactsListColumn.tsx | 4 +- web/src/features/messages/ChatListColumn.tsx | 41 +++----- web/src/features/messages/DetailColumn.tsx | 97 +++++++++++-------- web/src/features/settings/SettingsLayout.tsx | 21 ++-- 6 files changed, 97 insertions(+), 98 deletions(-) diff --git a/web/src/app/AppShell.tsx b/web/src/app/AppShell.tsx index 944a4fb2..cf9863c3 100644 --- a/web/src/app/AppShell.tsx +++ b/web/src/app/AppShell.tsx @@ -24,13 +24,15 @@ export function AppShell() { useEffect(() => { initCallSignaling(); }, []); - // "sys"/"slf" have their own authoritative unread source in entry_badges (bootstrap.ts) -- excluded - // here in case they ever also show up as regular Tinode subscriptions in `contacts`, which would - // otherwise double-count the same unread messages into this badge. + // "sys" has its own authoritative unread source in entry_badges (bootstrap.ts) -- excluded here + // in case it ever also shows up as a regular Tinode subscription in `contacts`, which would + // otherwise double-count the same unread messages into this badge. "slf" gets no such exclusion: + // 文件助手 (which used to own its own entry_badges count, file_assistant_unread_count) has been + // removed as a UI entry point entirely -- see ChatListColumn.tsx's PINNED_ENTRIES doc comment -- + // so "slf" is now just an ordinary contact row like any other, and should count normally here too. const unreadMessages = - contacts.reduce((sum, c) => (c.topic === 'sys' || c.topic === 'slf' ? sum : sum + (c.unread || 0)), 0) + - (bootstrap.data?.entry_badges?.system_unread_count || 0) + - (bootstrap.data?.entry_badges?.file_assistant_unread_count || 0); + contacts.reduce((sum, c) => (c.topic === 'sys' ? sum : sum + (c.unread || 0)), 0) + + (bootstrap.data?.entry_badges?.system_unread_count || 0); return (
diff --git a/web/src/app/NavRail.tsx b/web/src/app/NavRail.tsx index c4f8c2d8..7934b815 100644 --- a/web/src/app/NavRail.tsx +++ b/web/src/app/NavRail.tsx @@ -13,7 +13,6 @@ const TOP_ITEMS: NavRailItem[] = [ { to: '/messages', icon: 'chat_bubble', label: '消息' }, { to: '/contacts', icon: 'contacts', label: '通讯录' }, { to: '/favorites', icon: 'inventory_2', label: '收藏' }, - { to: '/files', icon: 'folder', label: '文件助手' }, ]; const BOTTOM_ITEMS: NavRailItem[] = [ @@ -21,14 +20,8 @@ const BOTTOM_ITEMS: NavRailItem[] = [ { to: '/settings', icon: 'settings', label: '设置' }, ]; -/** `/files` is `` (App.tsx) -- the real URL right after - * clicking "文件助手" is `/messages/slf`, which a plain (non-`end`) NavLink match makes "消息" - * light up for (a same-topicId prefix match) while "文件助手" itself (`to: '/files'`, a path that - * no longer exists once redirected) never matches at all. Overriding both items' active state off - * the real pathname instead of trusting NavLink's own prefix match fixes both sides of the mix-up. */ function isNavItemActive(to: string, pathname: string): boolean { - if (to === '/files') return pathname === '/messages/slf'; - if (to === '/messages') return pathname.startsWith('/messages') && pathname !== '/messages/slf'; + if (to === '/messages') return pathname.startsWith('/messages'); return pathname === to || pathname.startsWith(`${to}/`); } @@ -39,12 +32,9 @@ function NavRailButton({ item }: { item: NavRailItem }) { // Plain Link, not NavLink -- NavLink computes its OWN `aria-current` from ITS OWN prefix-match // logic unconditionally (confirmed by reading react-router's source: passing an `aria-current` // prop only changes the STRING used, not whether it's applied at all, which stays gated on - // NavLink's internal isActive). That's exactly `/files`'s own problem above -- NavLink's naive - // match would light up "消息" for `/messages/slf`, not "文件助手" -- so a second, independent - // aria-current computation fighting the same override this file already needed for `className` - // is a real, verifiable a11y bug (screen readers and sighted highlighting would disagree on - // which icon is current). Driving both off the one already-correct `isActive` avoids that - // entirely. + // NavLink's internal isActive), so a second, independent aria-current computation could in + // principle disagree with the `className` highlight computed by `isActive` below. Driving both + // off the one shared `isActive` value keeps them impossible to desync. 最近") first, then by `touched` (last activity) @@ -68,14 +65,7 @@ export function ChatListColumn() { const memberCountByTopic = new Map(groupsQuery.data?.map((g) => [g.topic, g.member_count])); const [query, setQuery] = useState(''); const [creatingGroup, setCreatingGroup] = useState(false); - // `slf` (self-chat) is permanently represented by the pinned 文件助手 entry above (see - // PINNED_ENTRIES's own doc comment) -- it also comes back as an ordinary contact once it has any - // real message history (e.g. the very first time something gets sent through 文件助手), which - // without this filter renders it a SECOND time as its own chronologically-sorted row further down. - // Confirmed live: with both rows present, navigating to /messages/slf (via either one) satisfies - // both rows' own "am I the active one" check at once, highlighting them simultaneously -- a real, - // reproducible duplicate-representation bug, not just a cosmetic double-highlight. - const sorted = [...contacts].filter((c) => c.topic !== 'slf').sort((a, b) => { + const sorted = [...contacts].sort((a, b) => { const pinDelta = (b.private?.pinned ? 1 : 0) - (a.private?.pinned ? 1 : 0); return pinDelta || (b.touched?.getTime() ?? 0) - (a.touched?.getTime() ?? 0); }); @@ -105,10 +95,7 @@ export function ChatListColumn() {
{pinned.map((entry) => { const unread = bootstrap.data?.entry_badges?.[entry.badgeKey] || 0; - // `/files` redirects to `/messages/slf`, so its "currently open" check is keyed off the - // resulting topicId rather than the route it navigates from, unlike `/messages/system` - // which is the real, final path. - const active = entry.route === '/files' ? topicId === 'slf' : location.pathname === entry.route; + const active = location.pathname === entry.route; return ( fetchAllPages((cursor) => getFriends('', cursor)) }); const friend = friendsQuery.data?.find((f) => f.user_id === topicId); - const profileQuery = useQuery({ queryKey: ['user-profile', topicId], queryFn: () => getUserProfile(topicId) }); + const profileQuery = useQuery({ queryKey: ['user-profile', topicId], queryFn: () => getUserProfile(topicId), enabled: !isSelf }); const idLine = [profileQuery.data?.public_account && `ID:${profileQuery.data.public_account}`, profileQuery.data?.region] .filter(Boolean) .join(' · '); @@ -160,28 +169,32 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta 发消息 - - + {isSelf ? null : ( + <> + + + + )}
@@ -208,7 +221,7 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta
- {remarkEditing ? ( + {isSelf ? null : remarkEditing ? (
备注 updateSettings.mutate({ mute_notifications: !settingsQuery.data?.mute_notifications })} />
-
- 加入黑名单 - blockMutation.mutate(!isBlocked)} /> -
+ {isSelf ? null : ( +
+ 加入黑名单 + blockMutation.mutate(!isBlocked)} /> +
+ )}
@@ -265,19 +280,23 @@ export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: Deta 清空聊天记录 - + {isSelf ? null : ( + + )}
- + {isSelf ? null : ( + + )} setErrorToast('')} /> diff --git a/web/src/features/settings/SettingsLayout.tsx b/web/src/features/settings/SettingsLayout.tsx index 67dc7be5..ba3c2212 100644 --- a/web/src/features/settings/SettingsLayout.tsx +++ b/web/src/features/settings/SettingsLayout.tsx @@ -19,17 +19,18 @@ const DIRECTORY = [ ]; /** - * README §8.7 (mobile "我的" page): 我的收藏/文件助手/通话记录 are grouped ABOVE the regular - * settings directory. Desktop never needs this list -- those three already have their own - * permanent NavRail icons there -- but on a phone-width viewport (bottom tab bar instead of a rail) - * there is otherwise NO way to reach `/favorites` or `/calls` at all short of typing the URL - * directly (confirmed live: BottomTabBar.tsx only has 消息/通讯录/我的, unchanged since Phase 1-2 - * when those two routes didn't exist yet) -- a real navigation gap this Phase 9 pass caught, not a - * cosmetic one. `/files` reuses the same `slf`-conversation redirect the desktop entry point does. + * README §8.7 (mobile "我的" page): 我的收藏/通话记录 are grouped ABOVE the regular settings + * directory. Desktop never needs this list -- those already have their own permanent NavRail icons + * there -- but on a phone-width viewport (bottom tab bar instead of a rail) there is otherwise NO + * way to reach `/favorites` or `/calls` at all short of typing the URL directly (confirmed live: + * BottomTabBar.tsx only has 消息/通讯录/我的, unchanged since Phase 1-2 when those two routes didn't + * exist yet) -- a real navigation gap this Phase 9 pass caught, not a cosmetic one. 文件助手 used to + * be a third entry here (reusing the same `slf`-conversation redirect the desktop NavRail icon did) + * but was removed along with every other entry point -- see ChatListColumn.tsx's PINNED_ENTRIES + * doc comment for why. */ const MOBILE_SHORTCUTS = [ { key: 'favorites', to: '/favorites', icon: 'inventory_2', label: '我的收藏', colorClass: 'text-accent' }, - { key: 'files', to: '/files', icon: 'folder', label: '文件助手', colorClass: 'text-success' }, { key: 'calls', to: '/calls', icon: 'phone', label: '通话记录', colorClass: 'text-warn' }, ]; @@ -131,8 +132,8 @@ export function SettingsLayout() { {/* Mobile hub (README §8.7, mock `07/1d`) -- a "me" landing page, not a settings directory: - tappable profile card (-> /settings/profile), 我的收藏/文件助手/通话记录 (this app's only - way to reach those three on a narrow viewport, see MOBILE_SHORTCUTS' own comment), then + tappable profile card (-> /settings/profile), 我的收藏/通话记录 (this app's only way to + reach those two on a narrow viewport, see MOBILE_SHORTCUTS' own comment), then 账号与安全/隐私 direct, and "设置" as its own second-level list rather than a 4th direct link -- these two screens genuinely don't share one flat structure in the mock. */}