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 : (
+ <>
+
+
+ >
+ )}
-
+ {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. */}