import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { Icon } from '@/app/Icon'; import { Toast } from '@/app/Toast'; import { fetchAllPages } from '@/lib/api/client'; import { getBlocks } from '@/lib/api/friends'; import { getMyPrivacy, updateMyPrivacy, type AddMeMethods } from '@/lib/api/profile'; const ADD_ME_LABELS: { key: keyof AddMeMethods; label: string }[] = [ { key: 'account', label: '通过用户 ID' }, { key: 'phone', label: '通过手机号' }, { key: 'qr', label: '通过二维码' }, { key: 'group', label: '通过群聊' }, ]; const VISIBILITY_OPTIONS = [ { value: 'everyone', label: '所有人' }, { value: 'friends', label: '仅好友' }, { value: 'nobody', label: '不展示' }, ]; function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: () => void; disabled?: boolean }) { return ( ); } /** * screens/07-profile-settings.dc.html#1c: 添加我的方式 (4 toggles, ungrouped -- no dividers between * them, just the section label above) sits in its own card; 黑名单 sits in a second card alone * (that mockup also puts "加我为好友时需要验证" there, but that toggle has NO backing field anywhere * in this response shape or the group-invite backend logic -- confirmed absent, not built). The * visibility/read-receipts rows below have no mockup at all (added once the real backend fields were * found during research) -- given their own third card rather than folding them into either of the * above, so a real capability the design never anticipated doesn't look like it was crammed in. */ export function PrivacySettingsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const [error, setError] = useState(''); const query = useQuery({ queryKey: ['my-privacy'], queryFn: getMyPrivacy }); const blocksQuery = useQuery({ queryKey: ['blocks'], queryFn: () => fetchAllPages((cursor) => getBlocks(cursor)) }); const mutation = useMutation({ mutationFn: (patch: Parameters[0]) => updateMyPrivacy(patch), // Merge, don't replace -- the doc's own worked example for this exact endpoint (line 2494) // returns a PARTIAL object (`{add_me_methods,read_receipts,version,updated_at}`, missing // `profile_visibility`/`last_seen_visibility`/`editable`). A blind replace would erase those // from the cache after every single save, most importantly `editable`: once it silently // disappeared, `const editable = query.data.editable || {}` re-derives an EMPTY object, which // re-enables every field a tenant policy had actually locked, until an unrelated refetch happens // to restore it. onSuccess: (data) => queryClient.setQueryData(['my-privacy'], (prev: typeof query.data) => (prev ? { ...prev, ...data } : data)), // Same reasoning as ProfileSettingsPage's own saveMutation: a version conflict shouldn't leave // the cached `version` stale, or every retry just resends the same rejected value forever. onError: (err) => { setError(err instanceof Error ? err.message : '设置失败'); queryClient.invalidateQueries({ queryKey: ['my-privacy'] }); }, }); const patch = (partial: Partial[0], 'version'>>) => { if (!query.data) return; mutation.mutate({ ...partial, version: query.data.version }); }; if (query.isLoading) return
加载中…
; // `isLoading` alone used to gate this: it turns false once the fetch SETTLES, including into an // error, but `query.data` stays undefined either way -- the page was falling through to the same // "加载中…" text forever on a genuine failure (a slow/dropped connection, not just "still loading"), // with no retry affordance at all. Same fix already applied to SystemMessagesView's own query. if (query.isError || !query.data) { return (
加载失败,请重试
); } const editable = query.data.editable || {}; return (

隐私

添加我的方式
{ADD_ME_LABELS.map((item, i) => (
{item.label} patch({ add_me_methods: { [item.key]: !query.data.add_me_methods[item.key] } })} />
))}
资料可见范围
{VISIBILITY_OPTIONS.map((opt) => ( ))}
最近在线可见范围
{VISIBILITY_OPTIONS.map((opt) => ( ))}
已读回执 patch({ read_receipts: !query.data.read_receipts })} />
setError('')} />
); }