import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Icon } from '@/app/Icon'; import { Toast } from '@/app/Toast'; import { getAllGroupMembers, getLatestGroupAnnouncement, readGroupAnnouncement, updateCurrentGroupAnnouncement, type GroupRole } from '@/lib/api/groups'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { formatEventTimestamp } from '@/lib/messages/time'; import { useEscapeKey } from '@/lib/hooks'; const ROLE_LABEL: Record = { owner: '群主', admin: '管理员', member: '' }; interface GroupAnnouncementModalProps { topicId: string; canEdit: boolean; onClose: () => void; } /** * README §4.8 (480px): publisher avatar/name/role tag + publish time, body text, read count, and an * 编辑 affordance for owner/admin. `PUT .../announcements/current` is optimistic-locked by * `version`, confirmed present in the GET response (doc line 1745, and re-confirmed live). Two * corrections from real responses (2026-09-08, real group `grpvrdHqKqkCq0`): the publisher comes * back as a flat `author: "usr..."` id, not the `published_by` object this file originally guessed * -- resolved here against the group's member list, same as any other bare user id in this API. * When nothing has been published yet, the endpoint still returns `200` with every field * zero-valued, never `404`/`null` -- checked below via `content` emptiness, not object existence (a * real bug this file had before this fix: `!query.data` was never true, so it rendered a blank * "announcement" -- empty name, empty date -- instead of the empty state). * `read_count`/`member_count` were confirmed ABSENT on 2026-09-08; the 2026-09-18 backend deploy * added them (see groups.ts's own comment on `GroupAnnouncement`), so this now renders "已读N/M" as * README §4.8 originally called for, and calls `readGroupAnnouncement` on open -- that REST endpoint * existed in groups.ts since before this fix but was never actually called from anywhere, so opening * an announcement never told the server "this member has read it" at all. */ export function GroupAnnouncementModal({ topicId, canEdit, onClose }: GroupAnnouncementModalProps) { const queryClient = useQueryClient(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(''); const [error, setError] = useState(''); useEscapeKey(onClose); const query = useQuery({ queryKey: ['group-announcement', topicId], queryFn: () => getLatestGroupAnnouncement(topicId) }); const membersQuery = useQuery({ queryKey: ['group-members', topicId], queryFn: () => getAllGroupMembers(topicId) }); const author = membersQuery.data?.find((m) => m.user_id === query.data?.author); // Fire-and-forget: viewing the announcement IS "reading" it, matching how the P2P/group message // read receipt (native `{note what:"read"}`) already works elsewhere in this app -- no explicit // "mark as read" button anywhere in the mock. Guarded on a real, non-empty `announcement_id` so // this doesn't fire for the zero-valued "nothing published yet" response, and keyed on that id (not // just "modal opened") so re-opening the SAME announcement doesn't re-send needlessly, though the // endpoint's own idempotency key means it would be harmless either way. const announcementId = query.data?.announcement_id; useEffect(() => { if (!announcementId) return; // Invalidate (not just fire-and-forget) so THIS viewer's own read immediately bumps the // displayed count instead of showing a stale pre-read number until some unrelated refetch. readGroupAnnouncement(topicId, announcementId) .then(() => queryClient.invalidateQueries({ queryKey: ['group-announcement', topicId] })) .catch(() => {}); }, [topicId, announcementId, queryClient]); const saveMutation = useMutation({ mutationFn: (content: string) => updateCurrentGroupAnnouncement(topicId, content, query.data?.version ?? 0), onSuccess: (data) => { queryClient.setQueryData(['group-announcement', topicId], data); // `GET .../settings` genuinely has no `announcement` field (see GroupDetailColumn.tsx's own // comment on this) -- the detail rail's own announcement preview actually reads // `group-summary`, so invalidating `group-settings` here never refreshed it; the preview text // was stuck at whatever it was before this save until something ELSE happened to touch summary. queryClient.invalidateQueries({ queryKey: ['group-summary', topicId] }); setEditing(false); }, onError: (err) => setError(err instanceof Error ? err.message : '发布失败'), }); const startEdit = () => { setDraft(query.data?.content || ''); setEditing(true); }; return (
e.stopPropagation()}>
群公告
{canEdit && !editing ? ( ) : null}
{editing ? (
{error ?
{error}
: null}