From 8f4b0de63695c8f93a519cdeac70bbe37822a934 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 16 Sep 2026 12:37:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E7=AC=AC9=E8=BD=AE=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=E6=89=B9=E6=AC=A12=E2=80=94=E2=80=94=E7=BE=A4?= =?UTF-8?q?=E8=81=8A=E5=9F=9F=E4=B8=80=E6=89=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 建群留空群名字直接被后端拒绝:placeholder写的"未填写时默认为成员 昵称组合"从未真正实现,现在留空时用已选成员昵称拼接生成默认名, 按MAX_TITLE_LENGTH截断 - 群文件上传按钮不受"群文件上传权限"治理开关约束:新增canUpload (和canInvite同一个算法),仅管理员时非管理员看不到上传入口 - 群管理弹层5个治理开关(进群验证/全员禁言/文件上传/私聊权限/邀请 权限)都没有读取后端返回的对应_editable字段,现在全部接上 - 禁言成员列表只拉第一页:新增getAllGroupMutes复用fetchAllPages, 和getAllGroupMembers同一个模式,人数多的群不会再静默截断 - 添加管理员没有前端数量上限校验:达到上限时按钮禁用并提示"已达 管理员人数上限",不再让用户选完人才被后端拒绝 - 清空聊天记录没有错误处理,失败时静默无提示,现在补上 Co-Authored-By: Claude Sonnet 5 --- web/src/features/groups/CreateGroupModal.tsx | 15 +++++++- web/src/features/groups/GroupAdminModal.tsx | 36 ++++++++++++------- web/src/features/groups/GroupDetailColumn.tsx | 13 +++++-- web/src/features/groups/GroupFilesModal.tsx | 28 ++++++++++----- web/src/lib/api/groups.ts | 8 +++++ 5 files changed, 75 insertions(+), 25 deletions(-) diff --git a/web/src/features/groups/CreateGroupModal.tsx b/web/src/features/groups/CreateGroupModal.tsx index b6aacab3..e02d73ff 100644 --- a/web/src/features/groups/CreateGroupModal.tsx +++ b/web/src/features/groups/CreateGroupModal.tsx @@ -63,8 +63,21 @@ export function CreateGroupModal({ onClose, onCreated }: CreateGroupModalProps) setError(''); let result: Awaited>; try { + // The input's own placeholder promises "未填写时默认为成员昵称组合" (defaults to a + // combination of member nicknames when left blank), but `name` has no such fallback + // server-side (unlike `description`, which the protocol doc explicitly allows empty) -- + // confirmed live: submitting a blank name gets rejected outright with MALFORMED_REQUEST, not + // silently defaulted. Truncated the same way the input field itself is (MAX_TITLE_LENGTH), + // with an ellipsis when it had to cut names short, so this can't itself produce an + // over-length name the server would also reject. + const trimmedName = name.trim(); + let finalName = trimmedName; + if (!finalName) { + const joined = [...selected.values()].join('、'); + finalName = joined.length > MAX_TITLE_LENGTH ? `${joined.slice(0, MAX_TITLE_LENGTH - 1)}…` : joined; + } result = await createGroup({ - name: name.trim(), + name: finalName, member_user_ids: [...selected.keys()], }); } catch (err) { diff --git a/web/src/features/groups/GroupAdminModal.tsx b/web/src/features/groups/GroupAdminModal.tsx index 01865078..77d4bff0 100644 --- a/web/src/features/groups/GroupAdminModal.tsx +++ b/web/src/features/groups/GroupAdminModal.tsx @@ -10,7 +10,7 @@ import { getAllGroupMembers, getGroup, getGroupGovernance, - getGroupMutes, + getAllGroupMutes, getGroupRoleLimits, getGroupSummary, muteGroupMembers, @@ -106,7 +106,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { queryFn: () => getAllGroupMembers(topicId, { role: 'admin' }), enabled: isOwner, }); - const mutesQuery = useQuery({ queryKey: ['group-mutes', topicId], queryFn: () => getGroupMutes(topicId) }); + const mutesQuery = useQuery({ queryKey: ['group-mutes', topicId], queryFn: () => getAllGroupMutes(topicId) }); const govMutation = useMutation({ mutationFn: (patch: Parameters[1]) => updateGroupGovernance(topicId, patch), @@ -133,7 +133,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { onClose={() => setView('main')} /> ); - if (view === 'mutes') return setView('main')} />; + if (view === 'mutes') return setView('main')} />; if (view === 'transfer') return setView('main')} />; return ( @@ -153,7 +153,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { setGov({ join_approval: !govQuery.data?.join_approval })} /> @@ -164,7 +164,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { setGov({ mute_all: !govQuery.data?.mute_all })} /> @@ -177,7 +177,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { { value: 'admin', label: '仅管理员' }, ]} onChange={(v) => setGov({ member_upload_files: v === 'all' })} - disabled={!govQuery.data || govMutation.isPending} + disabled={!govQuery.data || govMutation.isPending || govQuery.data?.member_upload_files_editable === false} />
@@ -189,7 +189,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { { value: 'friends', label: '仅好友' }, ]} onChange={(v) => setGov({ member_private_chat: v === 'all' })} - disabled={!govQuery.data || govMutation.isPending} + disabled={!govQuery.data || govMutation.isPending || govQuery.data?.member_private_chat_editable === false} />
{/* `governance.member_invite`/`member_invite_editable` (docs/IM-前后端对接文档.md:1600) is a @@ -206,7 +206,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { { value: 'admin', label: '仅管理员' }, ]} onChange={(v) => setGov({ member_invite: v === 'all' })} - disabled={!govQuery.data || govMutation.isPending} + disabled={!govQuery.data || govMutation.isPending || govQuery.data?.member_invite_editable === false} /> {/* Owner-only, same as 转让群主 below -- the backend's own permission check for this @@ -226,7 +226,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { @@ -384,10 +384,20 @@ function AdminsSubView({ ))} - + {(() => { + const atLimit = !!maxAdminCount && admins.length >= maxAdminCount; + return ( + + ); + })()} )} setError('')} /> diff --git a/web/src/features/groups/GroupDetailColumn.tsx b/web/src/features/groups/GroupDetailColumn.tsx index f271fe2f..8aafb690 100644 --- a/web/src/features/groups/GroupDetailColumn.tsx +++ b/web/src/features/groups/GroupDetailColumn.tsx @@ -119,6 +119,11 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: // (no extra request needed) -- live-verified this reflects the real value (flipped it via // GroupAdminModal's own PATCH, re-fetched `/settings`, confirmed it changed), not a frozen default. const canInvite = isAdmin || settingsQuery.data?.governance?.member_invite === true; + // Same gap as `canInvite` right above (same governance toggle pattern, same root cause: the + // "群文件上传权限" toggle in GroupAdminModal writes `member_upload_files` but nothing ever read it + // back) -- `GroupFilesModal`'s upload button was unconditionally visible/clickable regardless of + // this setting. + const canUpload = isAdmin || settingsQuery.data?.governance?.member_upload_files === true; // No `version` in the request -- see updateGroupMemberSettings's own doc comment for why sending // one 400s every single call (this was the actual root cause of "member-settings always 400", @@ -159,7 +164,11 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: const clearHistory = async () => { if (!(await requestConfirm({ message: '确定清空本群的聊天记录吗?此操作仅清除本地记录,不影响其他成员。', danger: true }))) return; - tinode.getTopic(topicId).delMessagesAll(false); + try { + await tinode.getTopic(topicId).delMessagesAll(false); + } catch (err) { + showError(err, '清空聊天记录失败,请重试'); + } }; const reportAbuse = () => { @@ -411,7 +420,7 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: {openModal === 'announcement' ? ( setOpenModal(null)} /> ) : null} - {openModal === 'files' ? setOpenModal(null)} /> : null} + {openModal === 'files' ? setOpenModal(null)} /> : null} {openModal === 'admin' ? setOpenModal(null)} /> : null} {openModal === 'qrcode' ? setOpenModal(null)} /> : null} diff --git a/web/src/features/groups/GroupFilesModal.tsx b/web/src/features/groups/GroupFilesModal.tsx index 9cadb67d..5d75d7c1 100644 --- a/web/src/features/groups/GroupFilesModal.tsx +++ b/web/src/features/groups/GroupFilesModal.tsx @@ -34,6 +34,11 @@ interface GroupFilesModalProps { * owner/admin has no way to remove another member's upload from this UI even though the server * would accept the request. */ isAdmin: boolean; + /** `governance.member_upload_files` (所有成员/仅管理员), computed by the caller alongside + * `isAdmin` the exact same way `GroupDetailColumn`'s own `canInvite` already is -- without this, + * the toggle in GroupAdminModal had zero effect on this button, the same class of bug + * `canInvite` was already fixed for. */ + canUpload: boolean; onClose: () => void; } @@ -60,7 +65,7 @@ function fileTypeFromMime(mime: string, name: string): string { * more fields than the protocol doc's own minimal example (see groups.ts's header comment), ported * from the old app's real, working upload flow rather than guessed from the doc. */ -export function GroupFilesModal({ topicId, isAdmin, onClose }: GroupFilesModalProps) { +export function GroupFilesModal({ topicId, isAdmin, canUpload, onClose }: GroupFilesModalProps) { const queryClient = useQueryClient(); const myUserId = useSessionStore((s) => s.myUserId); const [query, setQuery] = useState(''); @@ -139,14 +144,19 @@ export function GroupFilesModal({ topicId, isAdmin, onClose }: GroupFilesModalPr 群文件 {files.length}
{/* Mock (#1h): accent-blue `upload` icon + "上传" label, not a bare icon-only button in - the neutral gray this used to be. */} - + the neutral gray this used to be. Hidden (not just disabled) when governance says + only admins may upload and this viewer isn't one -- there's no "ask an admin" action + a plain member could take from here, so a visible-but-blocked button would only + invite a confusing click. */} + {canUpload ? ( + + ) : null} diff --git a/web/src/lib/api/groups.ts b/web/src/lib/api/groups.ts index 7fb7e9c8..c1f875df 100644 --- a/web/src/lib/api/groups.ts +++ b/web/src/lib/api/groups.ts @@ -252,6 +252,14 @@ export function getGroupMutes(topic: string, cursor = '') { return imGet<{ items: GroupMute[]; next_cursor?: string }>(`/groups/${encodeURIComponent(topic)}/mutes${cursor ? `?cursor=${cursor}` : ''}`); } +/** `getGroupMutes` alone only ever reads the first page -- unlike `getAllGroupMembers`'s own + * `fetchAllPages` wrapper right above, GroupAdminModal's mutes view called the single-page + * function directly, silently truncating both the list and its own header count once a group has + * enough muted members to span more than one page. */ +export function getAllGroupMutes(topic: string) { + return fetchAllPages((cursor) => getGroupMutes(topic, cursor)); +} + export function unmuteGroupMember(topic: string, userId: string) { return imDelete<{ user_id: string; muted: boolean }>(`/groups/${encodeURIComponent(topic)}/mutes/${encodeURIComponent(userId)}`); }