import { useLayoutEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { Avatar } from '@/app/Avatar';
import { Icon } from '@/app/Icon';
import { Toast } from '@/app/Toast';
import { fetchAllPages } from '@/lib/api/client';
import {
blockUser,
deleteFriend,
getBlocks,
getConversationSettings,
getFriends,
getUserProfile,
unblockUser,
updateConversationSettings,
updateFriendRemark,
} from '@/lib/api/friends';
import type { MediaType } from '@/lib/api/messages';
import { getMe } from '@/lib/api/profile';
import { useTinode } from '@/lib/tinode/hooks';
import { getP2PPeerUserId } from '@/lib/tinode/topics';
import { useCallStore } from '@/stores/calls';
import { requestConfirm } from '@/stores/confirmDialog';
import { notify } from '@/stores/globalToast';
import { useSessionStore } from '@/stores/session';
interface DetailColumnProps {
topicId: string;
peerName: string;
onClose?: () => void;
onOpenHistory: (tab: 'text' | MediaType) => void;
}
function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: () => void; disabled?: boolean }) {
return (
);
}
export function DetailColumn({ topicId, peerName, onClose, onOpenHistory }: DetailColumnProps) {
const navigate = useNavigate();
const tinode = useTinode();
const queryClient = useQueryClient();
const [remarkEditing, setRemarkEditing] = useState(false);
const [remarkDraft, setRemarkDraft] = useState('');
const [errorToast, setErrorToast] = useState('');
const showError = (err: unknown, fallback: string) => setErrorToast(err instanceof Error ? err.message : fallback);
// DetailColumn is reused across every contact when docked (no `key={topicId}`, same instance),
// so without this an in-progress remark edit for the PREVIOUS contact stays open and could be
// saved against the NEW one, and a leftover error toast would keep showing over the new contact
// misattributing the failure. useLayoutEffect (not useEffect) so this resolves before paint --
// this component never remounts on topicId change, so a post-paint effect can let one frame
// render with the previous contact's stale edit box still open.
useLayoutEffect(() => {
setRemarkEditing(false);
setRemarkDraft('');
setErrorToast('');
}, [topicId]);
// `slf` (self-chat, e.g. opened via a chat-list row once it has real history -- there is no
// "文件助手" entry point pointing at it anymore, see ChatListColumn.tsx's PINNED_ENTRIES doc
// comment) isn't a friend and isn't really "someone else" at all -- getUserProfile('slf') has no
// real user to look up, and every friend-only action below (remark/block/report/delete friend)
// is nonsensical pointed at yourself. Gated out entirely rather than left to silently no-op or
// error, confirmed live: this used to render "加入黑名单"/"投诉举报"/"删除好友" for your own notes
// to yourself.
const isSelf = topicId === 'slf';
const myUserId = useSessionStore((s) => s.myUserId);
// NOT topicId -- see getP2PPeerUserId's own doc comment. Every lookup below that means "the other
// user" (profile, friends-list match, block, remark) has to go through this, not the raw topic id,
// because the two stop being interchangeable the instant this conversation has any real history.
const peerUserId = isSelf ? topicId : getP2PPeerUserId(tinode.getTopic(topicId), myUserId);
const friendsQuery = useQuery({ queryKey: ['friends'], queryFn: () => fetchAllPages((cursor) => getFriends('', cursor)) });
const friend = friendsQuery.data?.find((f) => f.user_id === peerUserId);
const profileQuery = useQuery({
queryKey: ['user-profile', peerUserId],
queryFn: () => getUserProfile(peerUserId || ''),
// `!isSelf` alone isn't enough now that peerUserId can genuinely be undefined for a beat (the
// subscriber list not loaded yet) -- without also gating on it, this fires with `undefined` the
// instant isSelf is false, same class of guaranteed-404 bug this whole fix targets, just with
// the literal string "undefined" as the id instead of the p2p topic name.
enabled: !isSelf && !!peerUserId,
});
const idLine = [profileQuery.data?.public_account && `ID:${profileQuery.data.public_account}`, profileQuery.data?.region]
.filter(Boolean)
.join(' · ');
// Same real gap as ChatListColumn/ConversationView's avatars -- this panel's header tile only ever
// drew the letter fallback, even though `profileQuery`/`friendsQuery` right above already carry a
// real `avatar_file_id` (that's how ContactsListColumn/FriendProfileView show it correctly). `friend`
// (the `/friends` list, same source the contacts pages already trust) takes priority since it's the
// one confirmed accurate; `profileQuery` covers a P2P peer who isn't (or is no longer) a friend.
const meQuery = useQuery({ queryKey: ['me'], queryFn: getMe, enabled: isSelf });
const avatarFileId = isSelf ? meQuery.data?.profile.avatar_file_id : (friend?.avatar_file_id ?? profileQuery.data?.avatar_file_id);
const settingsQuery = useQuery({
queryKey: ['conversation-settings', topicId],
queryFn: () => getConversationSettings(topicId),
});
const updateSettings = useMutation({
mutationFn: (patch: { pinned?: boolean; mute_notifications?: boolean }) =>
updateConversationSettings(topicId, { ...patch, version: settingsQuery.data?.version ?? 0 }),
onSuccess: (data) => queryClient.setQueryData(['conversation-settings', topicId], data),
onError: (err) => showError(err, '设置失败,请重试'),
});
const remarkMutation = useMutation({
mutationFn: (remark: string) => updateFriendRemark(peerUserId, remark),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['friends'] });
setRemarkEditing(false);
},
onError: (err) => showError(err, '备注保存失败'),
});
const blockMutation = useMutation({
mutationFn: (blocked: boolean) => (blocked ? blockUser(peerUserId) : unblockUser(peerUserId)),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['blocks'] }),
onError: (err) => showError(err, '设置失败,请重试'),
});
const blocksQuery = useQuery({ queryKey: ['blocks'], queryFn: () => fetchAllPages((cursor) => getBlocks(cursor)) });
const isBlocked = !!blocksQuery.data?.some((b) => b.user.user_id === peerUserId);
const deleteFriendMutation = useMutation({
mutationFn: () => deleteFriend(peerUserId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['friends'] });
navigate('/messages');
},
onError: (err) => showError(err, '删除好友失败'),
});
const clearHistory = async () => {
if (!(await requestConfirm({ message: '确定清空与该联系人的聊天记录吗?此操作仅清除本地记录,不影响对方。', danger: true }))) return;
tinode.getTopic(topicId).delMessagesAll(false);
};
const startDeleteFriend = async () => {
if (!(await requestConfirm({ message: `确定删除好友 ${peerName} 吗?`, danger: true }))) return;
deleteFriendMutation.mutate();
};
const reportAbuse = () => {
notify('该功能暂未开放,如需投诉举报请联系客服。');
};
return (
);
}