diff --git a/web/src/features/groups/GroupAnnouncementModal.tsx b/web/src/features/groups/GroupAnnouncementModal.tsx index 21a6f274..9c957768 100644 --- a/web/src/features/groups/GroupAnnouncementModal.tsx +++ b/web/src/features/groups/GroupAnnouncementModal.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react'; +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, updateCurrentGroupAnnouncement, type GroupRole } from '@/lib/api/groups'; +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'; @@ -24,12 +24,15 @@ interface GroupAnnouncementModalProps { * 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. - * And `read_count`/`member_count` are confirmed ABSENT (not just unconfirmed), so dropped rather - * than kept as permanently-dead optional-chained code. 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). + * 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(); @@ -42,6 +45,22 @@ export function GroupAnnouncementModal({ topicId, canEdit, onClose }: GroupAnnou 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) => { @@ -120,6 +139,19 @@ export function GroupAnnouncementModal({ topicId, canEdit, onClose }: GroupAnnou 发布于 {formatEventTimestamp(new Date(query.data.published_at))}

{query.data.content}

+ {/* README §4.8. Both the flat fields and `read_summary` carry the same two numbers + (confirmed reading the backend's own payload builder) -- prefer the flat ones, + fall back to the nested object in case a future response only sends one shape. */} + {(() => { + const readCount = query.data.read_count ?? query.data.read_summary?.read_count; + const memberCount = query.data.member_count ?? query.data.read_summary?.member_count; + if (readCount == null || memberCount == null) return null; + return ( +

+ 已读 {readCount}/{memberCount} +

+ ); + })()} )} diff --git a/web/src/lib/api/groups.ts b/web/src/lib/api/groups.ts index 50e2f99c..df8dfd8b 100644 --- a/web/src/lib/api/groups.ts +++ b/web/src/lib/api/groups.ts @@ -409,11 +409,15 @@ export function getGroupSummary(topic: string) { /** Confirmed live (2026-09-08, real group `grpvrdHqKqkCq0`): `author` is a flat `usr...` id, not * the `published_by` object this file originally guessed -- resolve it against the group's member - * list for a display name/role, same as any other bare user id in this API. `read_count`/ - * `member_count` are confirmed ABSENT from the real response (not just unconfirmed), and when - * nothing has been published yet the endpoint still returns `200` with every field zero-valued - * (`announcement_id:"", author:"", content:"", published_at:"", version:0`), never `404`/`null` -- - * callers must check `content` for emptiness, not object existence. */ + * list for a display name/role, 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 (`announcement_id:"", + * author:"", content:"", published_at:"", version:0`), never `404`/`null` -- callers must check + * `content` for emptiness, not object existence. + * `read_count`/`member_count`/`read_summary` were confirmed ABSENT on 2026-09-08 (dropped from this + * type entirely, not just left optional) -- backend added them in the 2026-09-18 deploy (confirmed + * by reading `groupAnnouncementPayload` in `im_be/server/im_http.go`, which now sets all three). + * Optional here because an old cached response (or a tenant not yet on the new deploy) may still + * omit them. */ export interface GroupAnnouncement { announcement_id: string; topic: string; @@ -422,6 +426,9 @@ export interface GroupAnnouncement { published_at: string; author: string; permissions?: { can_edit?: boolean }; + read_count?: number; + member_count?: number; + read_summary?: { read_count: number; member_count: number }; } export function getLatestGroupAnnouncement(topic: string) {