import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Icon } from '@/app/Icon'; import { fetchAllPages } from '@/lib/api/client'; import { acceptGroupJoinRequest, getAllGroupMembers, getGroupJoinRequests, rejectGroupJoinRequest, type GroupJoinRequest } from '@/lib/api/groups'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { useEscapeKey } from '@/lib/hooks'; import { formatEventTimestamp } from '@/lib/messages/time'; interface JoinRequestsModalProps { topicId: string; onClose: () => void; } /** * README §4.6. Reject sends `{version}` ONLY -- no `reason` field, even though the protocol doc's * own example shows one (see lib/api/groups.ts's rejectGroupJoinRequest comment: the old app * confirmed live that the real server 400s on an unrecognized `reason` key). */ export function JoinRequestsModal({ topicId, onClose }: JoinRequestsModalProps) { const queryClient = useQueryClient(); const [handled, setHandled] = useState>({}); useEscapeKey(onClose); const query = useQuery({ queryKey: ['group-join-requests', topicId], // `'pending'`-only used to mean a request resolved before this session (or by another admin) // just never appeared here at all -- the render below already has a real "已同意/已拒绝" branch // keyed off the server's own `req.status`, it just never got fed anything but pending requests to // apply that branch to. README §4.6's own mock shows an already-resolved row persisting in the // list, matching `'all'`, not `'pending'`. queryFn: () => fetchAllPages((cursor) => getGroupJoinRequests(topicId, 'all', cursor)), }); // The applicant themself has no display name anywhere on the real response (see groups.ts's own // GroupJoinRequest comment) and isn't a member yet, so there's no list to resolve THEIR name from // either -- shown as a bare id, honestly, rather than guessed. The INVITER (mock: "王工 邀请") IS // already a member by definition, so that name can be resolved the same way GroupFilesModal's // uploader name and GroupAnnouncementModal's author name already do. const membersQuery = useQuery({ queryKey: ['group-members', topicId], queryFn: () => getAllGroupMembers(topicId) }); const memberName = (userId: string) => membersQuery.data?.find((m) => m.user_id === userId)?.display_name || userId; const invalidate = () => { queryClient.invalidateQueries({ queryKey: ['group-summary', topicId] }); queryClient.invalidateQueries({ queryKey: ['group-members', topicId] }); queryClient.invalidateQueries({ queryKey: ['group-members-preview', topicId] }); queryClient.invalidateQueries({ queryKey: ['group', topicId] }); }; const acceptMutation = useMutation({ mutationFn: (req: GroupJoinRequest) => acceptGroupJoinRequest(topicId, req.request_id, req.version), onSuccess: (_data, req) => { setHandled((prev) => ({ ...prev, [req.request_id]: 'accepted' })); invalidate(); }, }); const rejectMutation = useMutation({ mutationFn: (req: GroupJoinRequest) => rejectGroupJoinRequest(topicId, req.request_id, req.version), onSuccess: (_data, req) => { setHandled((prev) => ({ ...prev, [req.request_id]: 'rejected' })); // Unlike accept, nothing here changes group membership -- but `group-summary`'s // `pending_join_request_count` badge (read by GroupAdminModal's "进群申请" row) does need to // drop by one, and this previously invalidated nothing at all, leaving that badge stuck high. invalidate(); }, }); return (
e.stopPropagation()}>
进群申请
{query.isLoading ? (
加载中…
) : !query.data?.length ? (
暂无进群申请
) : ( query.data.map((req) => { const status = handled[req.request_id]; return (
{avatarLetter(req.user_id)} {req.user_id} {/* mock (#1c): "{邀请人} 邀请" when invited, else a source label -- NOT `source==='qr' ? ... : 邀请`, which used to show "邀请" for every non-qr source including a plain self-initiated search request that nobody actually invited (confirmed live: `source` really does take a `"search"` value distinct from an invite). `inviter_id` is the real signal for "was this an invite", not `source`. */} {req.inviter_id ? `${memberName(req.inviter_id)} 邀请` : req.source === 'qr' ? '扫码申请' : '申请加入'} {req.message ? ` · "${req.message}"` : ''} · {formatEventTimestamp(new Date(req.created_at))} {status || req.status !== 'pending' ? ( {status === 'accepted' || req.status === 'accepted' ? '已同意' : '已拒绝'} ) : ( )}
); }) )}
); }