feat(web): real P2P chat-settings detail panel

Replaces the detail-column placeholder with the actual "聊天设置" panel
(README §3.4): profile header, quick-action buttons, remark, pin/mute
toggles, blacklist, clear-history, delete-friend -- backed by real REST
endpoints (lib/api/friends.ts, ported from the old app's real
contact-management-view.jsx settings panel, not its unrelated mock
direct-message-view.jsx).

First real use of TanStack Query in this app (useQuery for
friends/conversation-settings/blocks, useMutation for the writes) --
previously only wired up in App.tsx with nothing consuming it yet.

Verified end to end: pin toggle PATCHes /conversations/{topic}/settings
and persists; remark PATCHes /friends/{userId} and reflects immediately
via query invalidation; blacklist PUT/DELETEs /blocks/{userId}.

Known minor gap: the drawer variant (960-1279px tier) stays mounted
(CSS-hidden) alongside the docked panel at >=1280px rather than
unmounting, so both instances' queries run -- harmless since TanStack
Query dedupes/shares the cache by key, but worth a follow-up cleanup.
Also still missing: the <960px "detail settings push as a full page"
variant the mobile spec calls for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-08 01:54:54 +08:00
co-authored by Claude Sonnet 5
parent f549248c08
commit 7f5756671d
3 changed files with 311 additions and 22 deletions
+11 -22
View File
@@ -2,24 +2,9 @@ import { useState } from 'react';
import { Outlet, useParams } from 'react-router-dom';
import { Icon } from '@/app/Icon';
import { ChatListColumn } from '@/features/messages/ChatListColumn';
function DetailColumn({ onClose }: { onClose?: () => void }) {
return (
<aside className="flex h-full w-full flex-none flex-col overflow-y-auto border-l border-border-panel bg-white md:w-[300px]">
<header className="flex h-15 flex-none items-center justify-between border-b border-list-item px-5">
<strong className="text-[15px] font-semibold">聊天设置</strong>
{onClose ? (
<button type="button" onClick={onClose} className="grid h-8 w-8 place-items-center text-text-tertiary-4">
<Icon name="close" />
</button>
) : null}
</header>
<div className="p-5 text-sm text-text-secondary">详情栏内容占位(阶段1实现)</div>
</aside>
);
}
import { DetailColumn } from '@/features/messages/DetailColumn';
import { useChatList } from '@/lib/tinode/hooks';
/**
* The 4-tier responsive split described in the design handoff:
@@ -33,6 +18,8 @@ export function MessagesLayout() {
const { topicId } = useParams();
const [detailOpen, setDetailOpen] = useState(false);
const showListOnNarrow = !topicId;
const contacts = useChatList();
const peerName = contacts.find((c) => c.topic === topicId)?.public?.fn || topicId || '';
return (
<div className="relative flex h-full w-full">
@@ -42,14 +29,16 @@ export function MessagesLayout() {
<div className={`h-full min-w-0 flex-1 ${showListOnNarrow ? 'hidden md:block' : ''}`}>
<Outlet context={{ onToggleDetail: () => setDetailOpen((v) => !v) }} />
</div>
<div className="hidden h-full lg:block">
<DetailColumn />
</div>
{detailOpen ? (
{topicId ? (
<div className="hidden h-full lg:block">
<DetailColumn topicId={topicId} peerName={peerName} />
</div>
) : null}
{detailOpen && topicId ? (
<div className="absolute inset-0 hidden md:block lg:hidden">
<div className="absolute inset-0 bg-[#22262d]/25" onClick={() => setDetailOpen(false)} />
<div className="absolute inset-y-0 right-0 shadow-[-8px_0_32px_rgba(0,0,0,0.12)]">
<DetailColumn onClose={() => setDetailOpen(false)} />
<DetailColumn topicId={topicId} peerName={peerName} onClose={() => setDetailOpen(false)} />
</div>
</div>
) : null}