diff --git a/web/src/App.tsx b/web/src/App.tsx index 8dd9d261..b5480df3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -41,6 +41,7 @@ const router = createBrowserRouter([ { path: 'contacts', element: }, { path: 'favorites', element: }, { path: 'files', element: }, + { path: 'chat/system', element: }, { path: 'calls', element: }, { path: 'settings', element: }, ], diff --git a/web/src/app/AppShell.tsx b/web/src/app/AppShell.tsx index 8c9f6120..25b9ab49 100644 --- a/web/src/app/AppShell.tsx +++ b/web/src/app/AppShell.tsx @@ -1,5 +1,6 @@ import { Outlet } from 'react-router-dom'; +import { useBootstrap } from '@/lib/api/hooks'; import { useChatList } from '@/lib/tinode/hooks'; import { BottomTabBar } from './BottomTabBar'; @@ -13,7 +14,11 @@ import { NavRail } from './NavRail'; */ export function AppShell() { const contacts = useChatList(); - const unreadMessages = contacts.reduce((sum, c) => sum + (c.unread || 0), 0); + const bootstrap = useBootstrap(); + const unreadMessages = + contacts.reduce((sum, c) => sum + (c.unread || 0), 0) + + (bootstrap.data?.entry_badges?.system_unread_count || 0) + + (bootstrap.data?.entry_badges?.file_assistant_unread_count || 0); return (
diff --git a/web/src/features/messages/ChatListColumn.tsx b/web/src/features/messages/ChatListColumn.tsx index a742d481..4e182a80 100644 --- a/web/src/features/messages/ChatListColumn.tsx +++ b/web/src/features/messages/ChatListColumn.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { Icon } from '@/app/Icon'; +import { useBootstrap } from '@/lib/api/hooks'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { formatMessagePreview } from '@/lib/messages/preview'; import { formatListTime } from '@/lib/messages/time'; @@ -10,20 +11,45 @@ import { useChatList } from '@/lib/tinode/hooks'; import { useSessionStore } from '@/stores/session'; /** - * Chat-list row sort: by `touched` (last activity) descending. Pinned-conversations-first isn't - * wired yet -- that's driven by the P2P conversation-settings API (GET/PATCH /conversations/{topic}/ - * settings {pinned}), which the detail panel doesn't build until its "置顶聊天" toggle lands. The - * fixed 系统消息/文件助手 rows (README §2.1) are also still missing -- pinning them correctly needs - * to know which topic name(s) those actually are on this backend, which hasn't been confirmed yet. + * README §2.1's two always-pinned utility rows. Real, confirmed backend facts (docs/IM-前后端对接 + * 文档.md:909): topic names are fixed ("slf"/"sys"), but 系统消息's unread comes from a separate + * notifications table (not the topic itself) and 文件助手's own dedicated 3-column layout (README + * §9.2) is Phase 8, not built yet -- so both route to the same SectionPlaceholder-backed "later + * phase" destinations the rest of the nav already uses (contacts/favorites/calls/settings), rather + * than a half-working ConversationView that assumes a regular P2P/group contact. */ +const PINNED_ENTRIES = [ + { + key: 'system_messages' as const, + badgeKey: 'system_unread_count' as const, + label: '系统消息', + icon: 'notifications', + route: '/chat/system', + colorClass: 'bg-warn', + }, + { + key: 'file_assistant' as const, + badgeKey: 'file_assistant_unread_count' as const, + label: '文件助手', + icon: 'folder', + route: '/files', + colorClass: 'bg-success', + }, +]; + +/** Chat-list row sort: by `touched` (last activity) descending. Pinned-conversations-first isn't + * wired yet -- that's driven by the P2P conversation-settings API (GET/PATCH /conversations/{topic}/ + * settings {pinned}), which the detail panel doesn't build until its "置顶聊天" toggle lands. */ export function ChatListColumn() { const { topicId } = useParams(); const myUserId = useSessionStore((s) => s.myUserId); const contacts = useChatList(); + const bootstrap = useBootstrap(); const [query, setQuery] = useState(''); const sorted = [...contacts].sort((a, b) => (b.touched?.getTime() ?? 0) - (a.touched?.getTime() ?? 0)); const trimmedQuery = query.trim(); const filtered = trimmedQuery ? sorted.filter((c) => (c.public?.fn || c.topic || '').toLowerCase().includes(trimmedQuery.toLowerCase())) : sorted; + const pinned = PINNED_ENTRIES.filter((entry) => bootstrap.data?.entries?.[entry.key]?.enabled); return (
+ {pinned.map((entry) => { + const unread = bootstrap.data?.entry_badges?.[entry.badgeKey] || 0; + return ( + + + + + + {entry.label} + + + {unread ? ( + + {unread} + + ) : null} + + + ); + })} {filtered.length === 0 ? (
diff --git a/web/src/features/messages/ConversationView.tsx b/web/src/features/messages/ConversationView.tsx index ff6e0028..346f8447 100644 --- a/web/src/features/messages/ConversationView.tsx +++ b/web/src/features/messages/ConversationView.tsx @@ -307,6 +307,7 @@ export function ConversationView() { myUserId={myUserId} selectMode={selectMode} selected={typeof msg.seq === 'number' && selectedSeqs.has(msg.seq)} + menuActive={typeof msg.seq === 'number' && menu?.msg.seq === msg.seq} onToggleSelect={toggleSelect} onContextMenu={(e, m) => setMenu({ x: e.clientX, y: e.clientY, msg: m })} /> diff --git a/web/src/features/messages/DetailColumn.tsx b/web/src/features/messages/DetailColumn.tsx index 37a41ec7..27301fe2 100644 --- a/web/src/features/messages/DetailColumn.tsx +++ b/web/src/features/messages/DetailColumn.tsx @@ -11,6 +11,7 @@ import { getBlocks, getConversationSettings, getFriends, + getUserProfile, unblockUser, updateConversationSettings, updateFriendRemark, @@ -53,6 +54,11 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps) const friendsQuery = useQuery({ queryKey: ['friends'], queryFn: () => getFriends() }); const friend = friendsQuery.data?.items.find((f) => f.user_id === topicId); + const profileQuery = useQuery({ queryKey: ['user-profile', topicId], queryFn: () => getUserProfile(topicId) }); + const idLine = [profileQuery.data?.public_account && `ID:${profileQuery.data.public_account}`, profileQuery.data?.region] + .filter(Boolean) + .join(' · '); + const settingsQuery = useQuery({ queryKey: ['conversation-settings', topicId], queryFn: () => getConversationSettings(topicId), @@ -98,6 +104,10 @@ export function DetailColumn({ topicId, peerName, onClose }: DetailColumnProps) deleteFriendMutation.mutate(); }; + const reportAbuse = () => { + window.alert('该功能暂未开放,如需投诉举报请联系客服。'); + }; + return (